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.
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.
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.
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
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;
}
}
})();
I have been having some issues sending JavaScript to an iFrame that uses a different port and after searching online it seems that the 'different port' part is causing the issue.
Here is the code sending JavaScript to the iFrame:
<script>
var network = document.getElementById("1").contentWindow.kiwi.components.Network();
$(".irc-channel-selector").click(function(event){
network.join('#' + $(this).attr('data-irc-channel'));
});
</script>
The iFrame does not use port 80 which appears to be the problem:
<iframe id="1" src="http://www.example.com:7888">
I understand that I can use something called postMessage to do the same as what I need but having read up on it online I'm not sure how it should be used, it seems pretty complex whereas I'm only used to basic JavaScript such as the code that I wrote above.
Can someone provide an example on how I can use this postMessage to mimic the behaviour above? Reading online documentation I do not understand how to use it in my scenario! :(
It's not very complicated to achieve this with postMessage. First, inside the iframe, you must expect a message:
var network = kiwi.components.Network();
function receive(event) {
// it is important to check the origin. Here I'm assuming that the parent window has that origin (same address, default port).
// if you don't check the origin any other site could include your iframe and send messages to it
if (event.origin === "http://www.example.com") {
var message = event.data;
// Assuming you might want different message types in the future. Otherwise message could be just the channel itself.
if (message.type === "JOIN") {
network.join(message.channel);
}
}
}
window.addEventListener("message", receive, false);
Now your iframe page is expecting a message to make it join a channel. The parent page can send that message with:
$(".irc-channel-selector").click(function(event){
var message = {
type: "JOIN",
channel: '#' + $(this).attr('data-irc-channel')
};
// Notice that again we're using the specific origin you used in your iframe
document.getElementById("1").contentWindow.postMessage(message, "http://www.example.com:7888");
});
Here's a far more simple fiddle where a message is sent to the same window, since I'd have to host a page somewhere to have an iframe in jsfiddle: https://jsfiddle.net/3h1Lw0j4/1/ -- Anyway it's useful to see how event.origin behaves.
I apologize in advance for the rudimentary question.
I have web page A that has a link to web page B on it. I need to locate the link to web page B (easy enough), and then store the HTML from web page B in a variable in my javascript script.
To store the HTML from web page A, I know it's a simple:
html_A = document.body.innerHTML;
How do I store the HTML from web page B? I believe I need to use AJAX correct? Or can I do it with javascript? And if it's the former, let's just assume the server for web page B allows for it.
Thank you in advance!
If youre trying to load HTML from a website that resides on a different server you will get a Cross-Origin Request Blocked Error. I dealt with this in the past and found a way to do it using YQL. Try it out:
//This code is located on Website A
$(document).ready(function() {
var websiteB_url = 'http://www.somewebsite.com/page.html';
var yql = '//query.yahooapis.com/v1/public/yql?q=' + encodeURIComponent('select * from html where url="' + websiteB_url + '"') + '&format=xml&callback=?';
$.getJSON(yql, function(data) {
function filterDataCUSTOM(data) {
data = data.replace(/<?\/body[^>]*>/g, '');// no body tags
data = data.replace(/[\r|\n]+/g, ''); // no linebreaks
return data;
}
if (data.results[0]) {
var res = filterDataCUSTOM(data.results[0]);
$("div#results").html(res);
} else {
console.log("Error: Could not load the page.");
}
});
});
This is only possible if web page B is on the same domain due to the same-origin policy security feature of all major browsers.
If both pages are on the same domain you could do
$.get("/uri/to/webpage/b").then(function(html) {
//do something with the html;
});
Note that the html will be available only once the ajax request finishes inside the .then(...) function. It will NOT be available on the line after this code block.
Hard to tell without knowing more about your situation but this is rarely the correct thing to do. You might want to look into $.fn.load() (is limited by SOP) or using iframes (is not limited by SOP) as one of these might be more appropriate.
I should note that the standard way of doing this when you need access to html from another domain is to pull it down on your webserver and then re-serve it from there. That being said, it is probably a violation of that website's terms of use.
tl;dr Can I execute un-trusted scripts on an iframe safely?
Back story:
I'm trying to make secure JSONP requests. A lot of older browsers do not support Web Workers which means that the current solution I came up with is not optimal.
I figured I could create an <iframe> and load a script inside it. That script would perform a JSONP request (creating a script tag), which would post a message to the main page. The main page would get the message, execute the callback and destroy the iframe. I've managed to do this sort of thing.
function jsonp(url, data, callback) {
var iframe = document.createElement("iframe");
iframe.style.display = "none";
document.body.appendChild(iframe);
var iframedoc = iframe.contentDocument || iframe.contentWindow.document;
sc = document.createElement("script");
sc.textContent = "(function(p){ cb = function(result){p.postMessage(result,'http://fiddle.jshell.net');};})(parent);";
//sc.textContent += "alert(cb)";
iframedoc.body.appendChild(sc);
var jr = document.createElement("script");
var getParams = ""; // serialize the GET parameters
for (var i in data) {
getParams += "&" + i + "=" + data[i];
}
jr.src = url + "?callback=cb" + getParams;
iframedoc.body.appendChild(jr);
window.onmessage = function (e) {
callback(e.data);
document.body.removeChild(iframe);
}
}
jsonp("http://jsfiddle.net/echo/jsonp/", {
foo: "bar"
}, function (result) {
alert("Result: " + JSON.stringify(result));
});
The problem is that since the iframes are on the same domain, the injected script still has access to the external scope through .top or .parent and such.
Is there any way to create an iframe that can not access data on the parent scope?
I want to create an iframe where scripts added through script tags will not be able to access variables on the parent window (and the DOM). I tried stuff like top=parent=null but I'm really not sure that's enough, there might be other workarounds. I tried running a for... in loop, but my function stopped working and I was unable to find out why.
NOTE:
I know optimally WebWorkers are a better isolated environment. I know JSONP is a "bad" technique (I even had some random guy tell me he'd never use it today). I'm trying to create a secure environment for scenarios where you have to perform JSONP queries.
You can't really delete the references, setting null will just silently fail and there is always a way to get the reference to the parent dom.
References like frameElement and frameElement.defaultView etc. cannot be deleted. Attempting to do so will either silently fail or throw exception depending on browser.
You could look into Caja/Cajita though.
tl;dr no
Any untrusted script can steal cookies (like a session id!) or read information from the DOM like the value of a credit card input field.
JavaScript relies on the security model that all code is trusted code. Any attempts at access from another domain requires explicit whitelisting.
If you want to sandbox your iframe you can serve the page from another domain. This does mean that you can't share a session or do any kind of communication because it can be abused. It's just like including an unrelated website. Even then there are possibilities for abuse if you allow untrusted JavaScript. You can for instance do: window.top.location.href = 'http://my.phishing.domain/';, the user might not notice the redirect.
For the past 2 months I have been experiencing Amazon Cloudfront intermittent failures (2-3 times a week) whereby the page would load from my web server but all the assets from the CDN would block in pending for minutes at the time (I confirmed that with shell curl from different datacenters some work some don't depending on the edge location - London?). Once the pending requests succeed all goes back to normal.
We have been reporting this to amazon but they always reply with "Don't expect reply from us. If gazillion people will complain only then will we consider looking into this" kind of message. Often it resumes normal operation before I'm done writing the support request.
I came to a conclusion that the best way to proceed due to lack of development time for migrating to other CDN is to add a script in the html header that will let us know whenever something similar happens. So say in the header try to download a tiny gif from the CDN if the request takes longer than N msec then call an arbitrary url within the root domain (for monitoring).
The question:
How does one reliably, across all popular browsers, request a file with callback on timeout. i.e.:
request file from CDN using AJAX - will not work due to cross-domain limitations?
setTimeout("callbackTimeout",2000) callbackTimeout(){getElementById() else ...HttpWebRequest...} - would that be blocked by pending HttpWebRequest request or will it work?
How else?
Thanks.
This has been briefly tested in IE.7&8, up to date FF on Windows & OSX as well as Chrome. I suggest you test it yourself. Minify! If you know better way of doing this please suggest your improvements. The way using i.e. script instead of an image has been considered and decided against probably mostly due to my ignorance.
The next version will write a cookie on timeout and the future requests will be handled on the server side (using relative asset path). The cookie will expire after say 30 minutes. Every consecutive timeout will renew that cookie. Not sure how I'll handle the first failover. Could be a redirect (not very elegant but simple). Perhaps I will figure out smarter way (possibly more elegant but more complex too).
<script type="text/javascript">
//<![CDATA[
// Absolute path to a picture on your CDN to be monitored
cdnImagePath = "http://YOURCDNADDRESS.net/empty.gif";
//this is relative path (cross domain limitation)
//will be followed by "timeout" or "other" as a reason i.e. /cdnMonitor.php?message=timeout
cdnMonitoringPath = "/cdnMonitor.php?message=";
// Recommended 3000 for 3 second(s) timeout
cdnTimeoutMilisec = 3000;
// Set to true to be notified after timeout (provides extra information)
cdnNotifyAfterTimeout = false;
// Handler methods
cdnOK = function(){
if (!cdnTimer && cdnNotifyAfterTimeout) cdnNotify('success');
}
cdnFail = function(reason){
if (reason != "timeout") {
if (cdnTimer) clearTimeout(cdnTimer);
message = "error"
} else {
message = reason;
}
cdnNotify(message);
}
cdnTimeout = function() {
cdnTimer = false;
if (cdnImage.complete == false) {
cdnFail("timeout");
}
}
cdnNotify = function(message) {
if (window.XMLHttpRequest) {
xmlhttp = new XMLHttpRequest();
xmlhttp.open("GET", cdnMonitoringPath + message, true);
xmlhttp.send();
} else {// code for IE6, IE5
xmlhttp=new ActiveXObject("Microsoft.XMLHTTP");
}
}
// Load test image and define event handlers
cdnTimer = setTimeout("cdnTimeout()", cdnTimeoutMilisec);
cdnImage = new Image();
cdnImage.onload = cdnOK;
cdnImage.onerror = cdnFail;
cdnImage.src = cdnImagePath + "?" + Math.floor(Math.random()*1000000);
//]]>
</script>
Also this is what I'll use for ad hoc monitoring on the server side cdnMonitor.php:
error_log(date('Y-m-d H:i:s.') .next(explode('.',microtime(1))). ' - '. $_GET['message'] . ' - '. $_SERVER['HTTP_X_REAL_IP']. ' - ' . $_SERVER['HTTP_USER_AGENT'] ."\n", 3, '/tmp/cdnMonitor.log');
You will need to change the "HTTP_X_REAL_IP" to REMOTE_ADDR or whatever suits your needs. I use reverse proxy so that's what I do.
Lastly I made some last minute changes in the post editor and might have broken something. Fingers crossed.