32

I want to load a shared worker with a user-script. The problem is the user-script is free, and has no business model for hosting a file - nor would I want to use a server, even a free one, to host one tiny file. Regardless, I tried it and I (of course) get a same origin policy error:

Uncaught SecurityError: Failed to construct 'SharedWorker': Script at
'https://cdn.rawgit.com/viziionary/Nacho-Bot/master/webworker.js'
cannot be accessed from origin 'http://stackoverflow.com'.

There's another way to load a web worker by converting the worker function to a string and then into a Blob and loading that as the worker but I tried that too:

var sharedWorkers = {};
var startSharedWorker = function(workerFunc){
    var funcString = workerFunc.toString();
    var index = funcString.indexOf('{');
    var funcStringClean = funcString.substring(index + 1, funcString.length - 1);
    var blob = new Blob([funcStringClean], { type: "text/javascript" });
    sharedWorkers.google = new SharedWorker(window.URL.createObjectURL(blob));
    sharedWorkers.google.port.start();
};

And that doesn't work either. Why? Because shared workers are shared based on the location their worker file is loaded from. Since createObjectURL generates a unique file name for each use, the workers will never have the same URL and will therefore never be shared.

How can I solve this problem?


Note: I tried asking about specific solutions, but at this point I think the best I can do is ask in a more broad manner for any solution to the problem, since all of my attempted solutions seem fundamentally impossible due to same origin policies or the way URL.createObjectURL works (from the specs, it seems impossible to alter the resulting file URL).

That being said, if my question can somehow be improved or clarified, please leave a comment.

Community
  • 1
  • 1
  • You can have a look at http://plnkr.co/edit/Vlv1A7m7FJlNkb7CtNOm?p=preview – Ha Hoang Aug 11 '16 at 00:26
  • If SharedWorker can be accessed from several browsing contexts, all those browsing contexts must share the exact same origin (same protocol, host, and port). Look at: https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers – Ha Hoang Aug 12 '16 at 14:17
  • @HaHoang I know shared web workers have to share the same origin. I've thoroughly researched the specs. –  Aug 12 '16 at 16:33
  • I'm pretty sure @HaHoang's point is **you can't do that**. If you could do what you're trying to do then you'd effectively defeat all the security they put around shared workers in the first place. – gman Aug 13 '16 at 09:28
  • If I'm not mistaken this can be doing by _mirroring wanted scripts on your server_ with `php curl` or another same. – imbearr Aug 13 '16 at 09:41
  • Looking at your edits, the original question on August 7 seems to be rather different. Did you answer that one, or is this a reformulation? – YellowBird Aug 13 '16 at 19:07
  • @YellowBird this is a reformulation of the original question. –  Aug 13 '16 at 20:31
  • @gman There actually might be a way. I've just been thinking about possibilities and it might be a long shot but just an idea: User scripts have access to special JS functions called GM functions. One of those functions is [`GM_xmlhttpRequest`](https://wiki.greasespot.net/GM_xmlhttpRequest) Maybe I could modify the way the shared worker loads the script, replacing its loading mechanism with this function which ignores same origin policies. Can we modify that part of the shared web worker? I'll see if I can figure it out in the mean time. –  Aug 13 '16 at 20:36
  • @gman What's your thoughts on guest271314's answer. Doesn't it achieve what you believed impossible? –  Aug 14 '16 at 20:40
  • Let me know who you think should be awarded the bounty. – Pekka Aug 17 '16 at 07:13
  • @Pekka웃 Highest voted. –  Aug 17 '16 at 08:11
  • Doesn't work at the moment :) – Pekka Aug 18 '16 at 13:09
  • 1
    @Pekka웃 Oh well I suppose selected since its a tie lol. Thank you very much for the bounty :) The script we discussed will be posted on meta by monday afternoon :) –  Aug 18 '16 at 20:06
  • You can encode the worker's script in a data-URL, rather than a blob. This **does** work (at least on Firefox v79-84). See my answer below. – Doin Jan 23 '21 at 09:08

4 Answers4

11

You can use fetch(), response.blob() to create an Blob URL of type application/javascript from returned Blob; set SharedWorker() parameter to Blob URL created by URL.createObjectURL(); utilize window.open(), load event of newly opened window to define same SharedWorker previously defined at original window, attach message event to original SharedWorker at newly opened windows.

javascript was tried at console at How to clear the contents of an iFrame from another iFrame, where current Question URL should be loaded at new tab with message from opening window through worker.port.postMessage() event handler logged at console.

Opening window should also log message event when posted from newly opened window using worker.postMessage(/* message */), similarly at opening window

window.worker = void 0, window.so = void 0;

fetch("https://cdn.rawgit.com/viziionary/Nacho-Bot/master/webworker.js")
  .then(response => response.blob())
  .then(script => {
    console.log(script);
    var url = URL.createObjectURL(script);
    window.worker = new SharedWorker(url);
    console.log(worker);
    worker.port.addEventListener("message", (e) => console.log(e.data));
    worker.port.start();

    window.so = window.open("https://stackoverflow.com/questions/" 
                            + "38810002/" 
                            + "how-can-i-load-a-shared-web-worker-" 
                            + "with-a-user-script", "_blank");

    so.addEventListener("load", () => {
      so.worker = worker;
      so.console.log(so.worker);
      so.worker.port.addEventListener("message", (e) => so.console.log(e.data));
      so.worker.port.start();
      so.worker.port.postMessage("hi from " + so.location.href);
    });

    so.addEventListener("load", () => {
      worker.port.postMessage("hello from " + location.href)
    })

  });

At console at either tab you can then use, e.g.; at How to clear the contents of an iFrame from another iFrame worker.postMessage("hello, again") at new window of current URL How can I load a shared web worker with a user-script?, worker.port.postMessage("hi, again"); where message events attached at each window, communication between the two windows can be achieved using original SharedWorker created at initial URL.

Community
  • 1
  • 1
guest271314
  • 1
  • 10
  • 82
  • 156
  • On which page was this example code suppose to be run? Im trying to test it, and Im getting the error in Chrome: `Uncaught (in promise) TypeError: Cannot read property 'addEventListener' of undefined(…)fetch.then.then.script @ VM1910:18` –  Aug 14 '16 at 10:03
  • @Viziionary The `javascript` was tried at `console` at http://stackoverflow.com/questions/33645685/how-to-clear-the-contents-of-an-iframe-from-another-iframe/33649349#33649349 . Which version of chrome did you try with? – guest271314 Aug 14 '16 at 14:54
  • I can confirm that this works. It's pretty impressive considering everyone else (after 250 question views) claimed it wasn't possible. I should point out there is one limitation, however this still satisfies most use-cases. The limitation being: If you need to have a page from `domain A` open, then for some reason open a *new* `domain A ` page *from* `domain B` or the user opens a page, it's impossible to establish a shared web worker between the two pages of `domain A`. The second `domain A` page must be opened *from* the first `domain A` page. Not a huge limitation but worth pointing out. –  Aug 14 '16 at 22:59
  • @Viziionary Did not try at different domains. Not certain follow `domain A` , `domain B` case? By `domain B` do you mean opening another `tab` at same origin? Considered requirement of communicating between tabs at same origin. Can you provide further description? – guest271314 Aug 14 '16 at 23:08
  • Yeah you met the requirements of the question. I'm only pointing out that if for some reason lets say: `Tab A (stackoverflow.com)` is open and *then* `Tab B (stackoverflow.com)` is opened **manually** by the user, rather than being opened by your code, we can't initiate a shared web worker between the two pages. Also let say I need to send information from a `google.com tab` to a `stackoverflow.com tab` by opening a tab from google.com with the information as the URL query, that new stackoverflow.com tab openned by the google.com tab cannot share a web worker with another stackoverlow.com tab. –  Aug 15 '16 at 00:52
  • 1
    @Viziionary It may be possible to communicate a reference to the existing `Tab A` `SharedWorker` to manually opened `Tab B` using `window.postMessage()`, `MessageChannel` or other method. Communicating between different origins is perhaps beyond the scope of present Question? Though, should also be possible. – guest271314 Aug 15 '16 at 01:00
  • 1
    All this solution does is to share an object between two pages, that **already** have complete javascript access to each others' objects. There's no advantage of using a shared worker here, you could just as well use a regular worker. The two windows don't even have separate message ports to/from the worker. So for nearly all use cases where a shared worker might be needed, this solution won't be adequate, sorry. – Doin Aug 06 '20 at 11:56
  • Already the `fetch` fails when trying to use this in my userscript with FF 84.0.2: "_Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at https://gitlab.com/gerib/userscripts/-/raw/master/janitor/janitor-navigation-sw.js. (Reason: CORS request did not succeed)._" – Gerold Broser Jan 21 '21 at 02:45
4

Precondition

  • As you've researched and as it has been mentioned in comments, SharedWorker's URL is subject to the Same Origin Policy.
  • According to this question there's no CORS support for Worker's URL.
  • According to this issue GM_worker support is now a WONT_FIX, and seems close enough to impossible to implement due to changes in Firefox. There's also a note that sandboxed Worker (as opposed to unsafeWindow.Worker) doesn't work either.

Design

What I suppose you want to achieve is a @include * userscript that will collect some statistics or create some global UI what will appear everywhere. And thus you want to have a worker to maintain some state or statistic aggregates in runtime (which will be easy to access from every instance of user-script), and/or you want to do some computation-heavy routine (because otherwise it will slow target sites down).

In the way of any solution

The solution I want to propose is to replace SharedWorker design with an alternative.

  • If you want just to maintain a state in the shared worker, just use Greasemonkey storage (GM_setValue and friends). It's shared among all userscript instances (SQLite behide the scenes).
  • If you want to do something computation-heavy task, to it in unsafeWindow.Worker and put result back in Greasemonkey storage.
  • If you want to do some background computation and it must be run only by single instance, there are number of "inter-window" synchronisation libraries (mostly they use localStorage but Greasemomkey's has the same API, so it shouldn't be hard to write an adapter to it). Thus you can acquire a lock in one userscript instance and run your routines in it. Like, IWC or ByTheWay (likely used here on Stack Exchange; post about it).

Other way

I'm not sure but there may be some ingenious response spoofing, made from ServiceWorker to make SharedWorker work as you would like to. Starting point is in this answer's edit.

Community
  • 1
  • 1
saaj
  • 17,056
  • 2
  • 75
  • 80
  • What's your thoughts on guest271314's answer. Doesn't it achieve what you believed impossible? –  Aug 14 '16 at 20:39
  • @Viziionary, did you get guest's answer to work in a userscript? (Or at all based on your comment there?) Especially with `@grant` set to a useful value (not `none`)? – Brock Adams Aug 14 '16 at 21:58
  • @BrockAdams It worked from the console. I didnt consider it working from console and not from the user script. Ill test that as soon as I have time and respond. –  Aug 14 '16 at 22:42
  • @BrockAdams See my new comment on guest's answer. –  Aug 14 '16 at 23:01
  • @Viziionary, so does that mean it *does* work for you from a userscript? With `@grant` set to a real value? None of these answers seem to meet the *question's* requirements. Although the suggestion to use `GM_setValue()` is the smartest/easiest/most-robust way to address what your real issue probably is. – Brock Adams Aug 14 '16 at 23:11
  • 1
    @BrockAdams I didnt try @grant / GM functions. I just verified that it worked. Maybe that's another issue with the solution, Ill have to look at that. also `GM_setValue()` is a function I haven't looked into yet. If you think you can use it to propose a superior solution to this one, feel free. I would change the selected answer if I believed a new answer was better. –  Aug 15 '16 at 00:02
  • 1
    @Viziionary Well, text alone in the answer is incomprehensible to me. I tried to read it several times. The idea demonstrated by the snippet doesn't make me change my belief. This is a very narrow case when you have an instance of the new window where user navigates, so you're able to attach an unserialisable object (shared worker in our case) to it. – saaj Aug 15 '16 at 16:45
3

I am pretty sure you want a different answer, but sadly this is what it boils down to.

Browsers implement same-origin-policies to protect internet users, and although your intentions are clean, no legit browser allows you to change the origin of a sharedWorker.

All browsing contexts in a sharedWorker must share the exact same origin

  • host
  • protocol
  • port

You cannot hack around this issue, I've trying using iframes in addition to your methods, but non will work.

Maybe you can put it your javascript file on github and use their raw. service to get the file, this way you can have it running without much efforts.

Update

I was reading chrome updates and I remembered you asking about this. Cross-origin service workers arrived on chrome!

To do this, add the following to the install event for the SW:

self.addEventListener('install', event => {
  event.registerForeignFetch({
    scopes: [self.registration.scope], // or some sub-scope
    origins: ['*'] // or ['https://example.com']
  });
});

Some other considerations are needed aswell, check it out:

Full link: https://developers.google.com/web/updates/2016/09/foreign-fetch?hl=en?utm_campaign=devshow_series_crossoriginserviceworkers_092316&utm_source=gdev&utm_medium=yt-desc

Community
  • 1
  • 1
Bamieh
  • 7,907
  • 3
  • 26
  • 47
  • User scripts have access to special JS functions called GM functions. One of those functions is [`GM_xmlhttpRequest`](https://wiki.greasespot.net/GM_xmlhttpRequest) Maybe I could modify the way the shared worker loads the script, replacing its loading mechanism with this function which ignores same origin policies. Can we modify that part of the shared web worker? Is the function which loads the worker script exposed anywhere? Does it use the page's native `xmlhttpRequest` ? –  Aug 13 '16 at 20:48
  • @Viziionary GM functions are greasemonkey and other browser JS injectors' custom functions, you cannot use those in production or on other people's machines. sadly you cannot change native code or play with it, you can put it in-front of a proxy, but this will not achieve what you're after. I will try to find ways for you, but i highly doubt it.. – Bamieh Aug 13 '16 at 23:14
  • Dude, you must not understand the premise of this question. Im writing a user-script. The user downloads Greasemonkey, add my script to his client, and when he gets on a site that my script targets, my script (the user-script asked about in the question) runs on the client browser, inserted in the target page. –  Aug 13 '16 at 23:31
  • Maybe I'm misunderstanding you. I'm not exactly sure. Sorry if that's the case. –  Aug 13 '16 at 23:37
  • What's your thoughts on guest271314's answer. Doesn't it achieve what you believed impossible? –  Aug 14 '16 at 20:39
  • @Viziionary I am sorry but i misunderstood what you were trying to achieve, if you have control over the user's environment (installing GM or others) you can disable the browser's same origin policy by setting the `--disable-web-security` on chrome. doesnt that solve your issue? – Bamieh Aug 14 '16 at 22:22
  • I dont think you can disable web security from within a script, right? Even a GM script. –  Aug 14 '16 at 22:41
  • @Viziionary you can create a plugin on firefox which allows cross origins, or use `GM_xmlhttpRequest` by adding `@grant GM_xmlhttpRequest` to your script. Convert response into a glob and use it as a SW script – Bamieh Aug 15 '16 at 08:05
  • @Viziionary i updated the answer, cross-origin SW are now a thing! – Bamieh Sep 24 '16 at 06:42
0

Yes you can! (here's how):

I don't know if it's because something has changed in the four years since this question was asked, but it is entirely possible to do exactly what the question is asking for. It's not even particularly difficult. The trick is to initialize the shared worker from a data-url that contains its code directly, rather than from a createObjectURL(blob).

This is probably most easily demonstrated by example, so here's a little userscript for stackoverflow.com that uses a shared worker to assign each stackoverflow window a unique ID number, displayed in the tab title. Note that the shared-worker code is directly included as a template string (i.e. between backtick quotes):

// ==UserScript==
// @name stackoverflow userscript shared worker example
// @namespace stackoverflow test code
// @version      1.0
// @description Demonstrate the use of shared workers created in userscript
// @icon https://stackoverflow.com/favicon.ico
// @include http*://stackoverflow.com/*
// @run-at document-start
// ==/UserScript==

(function() {
  "use strict";

  var port = (new SharedWorker('data:text/javascript;base64,' + btoa(
  // =======================================================================================================================
  // ================================================= shared worker code: =================================================
  // =======================================================================================================================

  // This very simple shared worker merely provides each window with a unique ID number, to be displayed in the title
  `
  var lastID = 0;
  onconnect = function(e)
  {
    var port = e.source;
    port.onmessage = handleMessage;
    port.postMessage(["setID",++lastID]);
  }

  function handleMessage(e) { console.log("Message Recieved by shared worker: ",e.data); }
  `
  // =======================================================================================================================
  // =======================================================================================================================
  ))).port;

  port.onmessage = function(e)
  {
    var data = e.data, msg = data[0];
    switch (msg)
    {
      case "setID": document.title = "#"+data[1]+": "+document.title; break;
    }
  }
})();

I can confirm that this is working on FireFox v79 + Tampermonkey v4.11.6117.

There are a few minor caveats:

Firstly, it might be that the page your userscript is targeting is served with a Content-Security-Policy header that explicitly restricts the sources for scripts or worker scripts (script-src or worker-src policies). In that case, the data-url with your script's content will probably be blocked, and OTOH I can't think of a way around that, unless some future GM_ function gets added to allow a userscript to override a page's CSP or change its HTTP headers, or unless the user runs their browser with an extension or browser settings to disable CSP (see e.g. Disable same origin policy in Chrome).

Secondly, userscripts can be defined to run on multiple domains, e.g. you might run the same userscript on https://amazon.com and https://amazon.co.uk. But even when created by this single userscript, shared workers obey the same-origin policy, so there should be a different instance of the shared worker that gets created for all the .com windows vs for all the .co.uk windows. Be aware of this!

Finally, some browsers may impose a size limit on how long data-urls can be, restricting the maximum length of code for the shared worker. Even if not restricted, the conversion of all the code for long, complicated shared worker to base64 and back on every window load is quite inefficient. As is the indexing of shared workers by extremely long URLs (since you connect to an existing shared worker based on matching its exact URL). So what you can do is (a) start with an initially very minimal shared worker, then use eval() to add the real (potentially much longer) code to it, in response to something like an "InitWorkerRequired" message passed to the first window that opens the worker, and (b) For added efficiency, pre-calculate the base-64 string containing the initial minimal shared-worker bootstrap code.

Here's a modified version of the above example with these two wrinkles added in (also tested and confirmed to work), that runs on both stackoverflow.com and en.wikipedia.org (just so you can verify that the different domains do indeed use separate shared worker instances):

// ==UserScript==
// @name stackoverflow & wikipedia userscript shared worker example
// @namespace stackoverflow test code
// @version      2.0
// @description Demonstrate the use of shared workers created in userscript, with code injection after creation
// @icon https://stackoverflow.com/favicon.ico
// @include http*://stackoverflow.com/*
// @include http*://en.wikipedia.org/*
// @run-at document-end
// ==/UserScript==

(function() {
  "use strict";

  // Minimal bootstrap code used to first create a shared worker (commented out because we actually use a pre-encoded base64 string created from a minified version of this code):
/*
// ==================================================================================================================================
{
  let x = [];
  onconnect = function(e)
  {
    var p = e.source;
    x.push(e);
    p.postMessage(["InitWorkerRequired"]);
    p.onmessage = function(e)  // Expects only 1 kind of message:  the init code.  So we don't actually check for any other sort of message, and page script therefore mustn't send any other sort of message until init has been confirmed.
    {
      (0,eval)(e.data[1]);  // (0,eval) is an indirect call to eval(), which therefore executes in global scope (rather than the scope of this function). See http://perfectionkills.com/global-eval-what-are-the-options/ or https://stackoverflow.com/questions/19357978/indirect-eval-call-in-strict-mode
      while(e = x.shift()) onconnect(e);  // This calls the NEW onconnect function, that the eval() above just (re-)defined.  Note that unless windows are opened in very quick succession, x should only have one entry.
    }
  }
}
// ==================================================================================================================================
*/

  // Actual code that we want the shared worker to execute.  Can be as long as we like!
  // Note that it must replace the onconnect handler defined by the minimal bootstrap worker code.
  var workerCode =
// ==================================================================================================================================
`
  "use strict";  // NOTE: because this code is evaluated by eval(), the presence of "use strict"; here will cause it to be evaluated in it's own scope just below the global scope, instead of in the global scope directly.  Practically this shouldn't matter, though: it's rather like enclosing the whole code in (function(){...})();
  var lastID = 0;
  onconnect = function(e)  // MUST set onconnect here; bootstrap method relies on this!
  {
    var port = e.source;
    port.onmessage = handleMessage;
    port.postMessage(["WorkerConnected",++lastID]);  // As well as providing a page with it's ID, the "WorkerConnected" message indicates to a page that the worker has been initialized, so it may be posted messages other than "InitializeWorkerCode"
  }

  function handleMessage(e)
  {
    var data = e.data;
    if (data[0]==="InitializeWorkerCode") return;  // If two (or more) windows are opened very quickly, "InitWorkerRequired" may get posted to BOTH, and the second response will then arrive at an already-initialized worker, so must check for and ignore it here.
    // ...
    console.log("Message Received by shared worker: ",e.data);  // For this simple example worker, there's actually nothing to do here
  }
`;
// ==================================================================================================================================

  // Use a base64 string encoding minified version of the minimal bootstrap code in the comments above, i.e.
  // btoa('{let x=[];onconnect=function(e){var p=e.source;x.push(e);p.postMessage(["InitWorkerRequired"]);p.onmessage=function(e){(0,eval)(e.data[1]);while(e=x.shift()) onconnect(e);}}}');
  // NOTE:  If there's any chance the page might be using more than one shared worker based on this "bootstrap" method, insert a comment with some identification or name for the worker into the minified, base64 code, so that different shared workers get unique data-URLs (and hence don't incorrectly share worker instances).

  var port = (new SharedWorker('data:text/javascript;base64,e2xldCB4PVtdO29uY29ubmVjdD1mdW5jdGlvbihlKXt2YXIgcD1lLnNvdXJjZTt4LnB1c2goZSk7cC5wb3N0TWVzc2FnZShbIkluaXRXb3JrZXJSZXF1aXJlZCJdKTtwLm9ubWVzc2FnZT1mdW5jdGlvbihlKXsoMCxldmFsKShlLmRhdGFbMV0pO3doaWxlKGU9eC5zaGlmdCgpKSBvbmNvbm5lY3QoZSk7fX19')).port;

  port.onmessage = function(e)
  {
    var data = e.data, msg = data[0];
    switch (msg)
    {
      case "WorkerConnected": document.title = "#"+data[1]+": "+document.title; break;
      case "InitWorkerRequired": port.postMessage(["InitializeWorkerCode",workerCode]); break;
    }
  }
})();
Doin
  • 6,230
  • 3
  • 31
  • 31
  • I tried the first sample code and it works but you're not loading the worker's code from somewhere, it's included in the script itself. And I don't get why the `...;base64,' + btoa(...)` is necessary. – Gerold Broser Jan 22 '21 at 09:15
  • @Gerold Yes that's the entire point: All code **MUST** be in the userscript itself, because a userscript is not associated with any server that its author could make a script file available on. The `...;base64,' + btoa(...)` is necessary because you're including the worker code in a data-URL, and URLs can contain only a limited set of characters: If you didn't convert the code to base64, it wouldn't be valid in a URL. – Doin Jan 23 '21 at 08:55