I have an iframe embedded into a legacy application. Inside of the iframe I need to do oauth (I'm capturing authentication tokens for later use). In order to do oauth from within an iframe I need to use a popup window. What I need is for that popup window to communicate back to the iframe when oauth completes so the page within the iframe can refresh its data. I need to support IE 10+, Chrome, and Firefox, and everything I've tried works fine in Chrome/Firefox but fails in IE. My current solution (which almost works) looks like this:
/** From within the iframe **/
var storageName = 'SoProOauth';
var addAccountDeferred;
$window.addEventListener('storage', function (evt) {
if (evt.key == storageName) {
localStorage.removeItem(storageName);
getAccounts().then(function () {
addAccountDeferred.resolve();
});
}
});
// ... later ...
addAccount: function (type) {
addAccountDeferred = $q.defer();
var uri = util.format('/oauth/%s/connect?auth=%s&return_uri=%s',
type, configuration.get('auth'), encodeURI('/configure/oauthcomplete'));
if (Boolean(configuration.get('debug'))) {
uri += '&debug=true';
}
localStorage.removeItem(storageName);
var oauthWindow = $window.open(uri, 'oauth', 'width=800,height=600');
if (oauthWindow && oauthWindow.focus) oauthWindow.focus();
return addAccountDeferred.promise;
}
/** From within the popup **/
localStorage.setItem('SoProOauth', 'done');
$timeout(function () {
setInfo('This window can be closed');
$window.open('', '_self', '');
$window.close();
}, 500);
Basically, I'm writing to local storage from within the popup window, and listening for the write event in the parent window. This almost works - it works great in my local test environment but fails when I deploy it to the dev cluster. There could be two reasons why it fails: 1) when I run locally everything is over HTTP, but in the dev cluster it is over HTTPS (worse, the page that hosts the iframe is HTTP but the iframe contents are HTTPS), or 2) when I run locally everything is localhost, but in the dev cluster the iframe host is one machine (local intranet) and the contents of the iframe are public internet (AWS).
Other things I've tried: cookies (same result - works locally but fails deployed), and all of the window properties (window.opener, window.parent, and testing for window.closed() from within the iframe) - these don't work in IE as things get reset once the domain changes (which happens because we're doing oauth).
As you can see the UI code is AngularJS, but what you can't see is it is hosted by Node.js. So I've considered using something like socket.io but it seems so heavy-hitting - I really just need to send a quick event from the popup to the parent within the iframe. Any thoughts?
Related
I'm having problems with a piece of code that has worked before for years, but seems to have stopped working now.
I'm opening a window with a login form and I'm listening via a WebSocket for events regarding that login. After the login was successful, I want to close the window (that my script has opened and kept the reference to) after a short moment. I'm using the following code:
const windowManager = {
window: null,
eventType: null,
}
function openWindow({ url, eventType }) {
windowManager.window = window.open(url)
windowManager.eventType = eventType
}
function closeWindow({ eventType }) {
if (windowManager.window && windowManager.eventType == eventType) {
setTimeout(() => {
windowManager.window && windowManager.window.close()
windowManager.window = null
}, 100)
}
}
I have confirmed that windowManager.window.close() is called and does not thrown an error. I have also extracted the code from the application and tested it separately and it still won't close the window. As I said, this piece of code has worked before and was not changed in the past two years or so.
I'm using the following browsers:
Safari 15.3
Firefox 97.0b9 (Developer Edition)
Chromium 94.0.4606.61
I'm grateful for any pointers which could help resolve this issue. Thanks a lot!
After figuring out that the above code worked totally fine with other sites like Google or GitHub, I found that the Cross-Origin-Opener-Policy header in our auth backend (which was the site that was opened with the code) is the culprit. We had just updated Helmet to version 5 which added the header by default.
Our solution was to set Cross-Origin-Opener-Policy to same-origin-allow-popups on both source and target window (which are hosted on the same origin, but served by different servers). It also worked when setting it to unsafe-none for the target window without setting it at all on the source window.
What's the simplest way to launch Firefox, load a 3rd party website (which I'm authorised to "automate"), and run some "privileged" APIs against that site? (e.g: nsIProgressListener, nsIWindowMediator, etc).
I've tried a two approaches:
Create a tabbed browser using XULrunner, "plumbing" all the appropriate APIs required for the 3rd party site to open new windows, follow 302 redirects, etc. Doing it this way, it's an aweful lot of code, and requires (afaict) that the user installs the app, or runs Firefox with -app. It's also extremely fragile. :-/
Launch Firefox passing URL of the 3rd party site, with MozRepl already listening. Then shortly after startup, telnet from the "launch" script to MozRepl, use mozIJSSubScriptLoader::loadSubScript to load my code, then execute my code from MozRepl in the context of the 3rd party site -- this is the way I'm currently doing it.
With the first approach, I'm getting lots of security issues (obviously) to work around, and it seems like I'm writing 10x more browser "plumbing" code then automation code.
With the second approach, I'm seeing lots of "timing issues", i.e:
the 3rd party site is somehow prevented from loading by MozRepl (or the execution of the privileged code I supply)???, or
the 3rd party site loads, but code executed by MozRepl doesn't see it load, or
the 3rd party site loads, and MozRepl isn't ready to take requests (despite other JavaScript running in the page, and port 4242 being bound by the Firefox process),
etc.
I thought about maybe doing something like this:
Modify the MozRepl source in some way to load privileged JavaScript from a predictable place in the filesystem at start-up (or interact with Firefox command-line arguments) and execute it in the context of the 3rd party website.
... or even write another similar add-on which is more dedicated to the task.
Any simpler ideas?
Update:
After a lot of trial-and-error, answered my own question (below).
I found the easiest way was to write a purpose-built Firefox extension!
Step 1. I didn't want to do a bunch of unnecessary XUL/addon related stuff that wasn't necessary; A "Bootstrapped" (or re-startless) extension needs only an install.rdf file to identify the addon, and a bootstrap.js file to implement the bootstrap interface.
Bootstrapped Extension: https://developer.mozilla.org/en-US/docs/Extensions/Bootstrapped_extensions
Good example: http://blog.fpmurphy.com/2011/02/firefox-4-restartless-add-ons.html
The bootstrap interface can be implemented very simply:
const path = '/PATH/TO/EXTERNAL/CODE.js';
const Cc = Components.classes;
const Ci = Components.interfaces;
const Cu = Components.utils;
var loaderSvc = Cc["#mozilla.org/moz/jssubscript-loader;1"];
.getService(Ci.mozIJSSubScriptLoader);
function install() {}
function uninstall() {}
function shutdown(data, reason) {}
function startup(data, reason) { loaderSvc.loadSubScript("file://"+path); }
You compile the extension by putting install.rdf and bootstrap.js into the top-level of a new zip file, and rename the zip file extension to .xpi.
Step 2. To have a repeatable environment for production & testing, I found the easiest way was to launch Firefox with a profile dedicated to the automation task:
Launch the Firefox profile manager: firefox -ProfileManager
Create a new profile, specifying the location for easy re-use (I called mine testing-profile) and then exit the profile manager.
Remove the new profile from profiles.ini in your user's mozilla config (so that it won't interfere with normal browsing).
Launch Firefox with that profile: firefox -profile /path/to/testing-profile
Install the extension from the file-system (rather than addons.mozilla.org).
Do anything else needed to prepare the profile. (e.g: I needed to add 3rd party certificates and allow pop-up windows for the relevant domain.)
Leave a single about:blank tab open, then exit Firefox.
Snapshot the profile: tar cvf testing-profile-snapshot.tar /path/to/testing-profile
From that point onward, every time I run the automation, I unpack testing-profile-snapshot.tar over the existing testing-profile folder and run firefox -profile /path/to/testing-profile about:blank to use the "pristine" profile.
Step 3. So now when I launch Firefox with the testing-profile it will "include" the external code at /PATH/TO/EXTERNAL/CODE.js on each start-up.
NOTE: I found that I had to move the /PATH/TO/EXTERNAL/ folder elsewhere during step 2 above, as the external JavaScript code would be cached (!!! - undesirable during development) inside the profile (i.e: changes to the external code wouldn't be seen on next launch).
The external code is privileged and can use any of the Mozilla platform APIs. There is however an issue of timing. The moment-in-time at which the external code is included (and hence executed) is one at which no Chrome window objects (and so no DOMWindow objects) yet exist.
So then we need to wait around until there's a useful DOMWindow object:
// useful services.
Cu.import("resource://gre/modules/Services.jsm");
var loader = Cc["#mozilla.org/moz/jssubscript-loader;1"]
.getService(Ci.mozIJSSubScriptLoader);
var wmSvc = Cc["#mozilla.org/appshell/window-mediator;1"]
.getService(Ci.nsIWindowMediator);
var logSvc = Cc["#mozilla.org/consoleservice;1"]
.getService(Ci.nsIConsoleService);
// "user" code entry point.
function user_code() {
// your code here!
// window, gBrowser, etc work as per MozRepl!
}
// get the gBrowser, first (about:blank) domWindow,
// and set up common globals.
var done_startup = 0;
var windowListener;
function do_startup(win) {
if (done_startup) return;
done_startup = 1;
wm.removeListener(windowListener);
var browserEnum = wm.getEnumerator("navigator:browser");
var browserWin = browserEnum.getNext();
var tabbrowser = browserWin.gBrowser;
var currentBrowser = tabbrowser.getBrowserAtIndex(0);
var domWindow = currentBrowser.contentWindow;
window = domWindow.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIWebNavigation)
.QueryInterface(Ci.nsIDocShellTreeItem)
.rootTreeItem.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindow);
gBrowser = window.gBrowser;
setTimeout = window.setTimeout;
setInterval = window.setInterval;
alert = function(message) {
Services.prompt.alert(null, "alert", message);
};
console = {
log: function(message) {
logSvc.logStringMessage(message);
}
};
// the first domWindow will finish loading a little later than gBrowser...
gBrowser.addEventListener('load', function() {
gBrowser.removeEventListener('load', arguments.callee, true);
user_code();
}, true);
}
// window listener implementation
windowListener = {
onWindowTitleChange: function(aWindow, aTitle) {},
onCloseWindow: function(aWindow) {},
onOpenWindow: function(aWindow) {
var win = aWindow.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowInternal || Ci.nsIDOMWindow);
win.addEventListener("load", function(aEvent) {
win.removeEventListener("load", arguments.callee, false);
if (aEvent.originalTarget.nodeName != "#document") return;
do_startup();
}
};
// CODE ENTRY POINT!
wm.addListener(windowListener);
Step 4. All of that code executes in the "global" scope. If you later need to load other JavaScript files (e.g: jQuery), call loadSubscript explicitly within the null (global!) scope
function some_user_code() {
loader.loadSubScript.call(null,"file:///PATH/TO/SOME/CODE.js");
loader.loadSubScript.call(null,"http://HOST/PATH/TO/jquery.js");
$ = jQuery = window.$;
}
Now we can use jQuery on any DOMWindow by passing <DOMWindow>.document as the second parameter to the selector call!
Suppose there is a web site has a global namespace Q = {}, and there is a function under it: Q.foo
I'd like to overwrite this function in my chrome extension, so when the web page calls Q.foo, it would do what I like.
I tried to write:
Q.foo = function(){
alert("over written");
}
with content script. But it doesn't work....
thanks.
The main problem iis that a chrome extension exists in a separated enviornment which was created so that extension developers cant screw with the existing page's javascript and vice versa.
However geeky people can do this:
document.head.innerHTML += '<script>Q.foo = function(){alert("over written");}</script>';
Basically what this does is that it appends a script tag into the dom which is then instantly eval'd in the context of the page.
Q.prototype.foo = function(){
alert("over written");
}
I have an iframe FB app. We have three places where we develop it: My localhost, stage server where we test the app, production server. Localhost and production have HTTPS. Localhost and stage apps have sandbox mode enabled. All versions of app are identical, code is the same. Stage and production are totally the same server machine with the same settings except of the HTTPS.
Now what is happening only at my stage server app: When I click on something where jQuery UI dialog should be summoned, it raises following error in my Firebug: Permission denied to access property 'Arbiter'. No dialog is summoned then. It's raised in somehow dynamically loaded canvas_proxy.php, within this code:
/**
* Parses the fragment and calls Arbiter.inform(method, params)
*
* #author ptarjan
*/
function doFragmentSend() {
var
location = window.location.toString(),
fragment = location.substr(location.indexOf('#') + 1),
params = {},
parts = fragment.split('&'),
i,
pair;
lowerPageDomain();
for (i=0; i<parts.length; i++) {
pair = parts[i].split('=', 2);
params[decodeURIComponent(pair[0])] = decodeURIComponent(pair[1]);
}
var p = params.relation ? resolveRelation(params.relation) : parent.parent;
// The user is not inside a frame (probably testing on their own domain)
if (p == parent || !p.Arbiter || !p.JSON) {
return;
}
p.Arbiter.inform(
'Connect.Unsafe.'+params.method,
p.JSON.parse(params.params),
getBehavior(p, params.behavior));
}
The line if (p == parent || !p.Arbiter || !p.JSON) { raises it. My script code linking the JS API looks like this:
<script src="https://connect.facebook.net/en_US/all.js#appId=APPID"></script>
Have anyone any clue why this could be happening? I found this and this, but these issues doesn't seem to be helpful to me (or I just don't get it). Could it be because of the HTTPS? Why it worked the day before yesterday? I am desperate :-(
whenever you have a permission denied message and you are dealing with frames or iframes, it's a document domain issue. One document belongs to domain x and the other is domain y. And notice that www.domain.com and domain.com are not the same document domains!
When you are tapping into the DOM of one framed document from another one, (whether it is for the purpose of changing the values of a page element or simply reading the values of some hidden variable or url etc), you will get a permission denied message unless both framed documents are served from the same/identical domains.
So, if one frame belongs to www.mydomain.com and the other happens to be just mydomain.com or www.someotherdomain.com, you get that bloody permission denied error.
And there is no way around it. And If there were, the identity theft problem would have sky-rocketed in no time.
I just found out that the Screen Capture by Google extension makes my website's window.onresize event not fire.
I want to perform a javascript check to see if the user has ScreenCapture installed and if so, warn the user of the problem.
A year ago I think I heard of some javascript code that could do this, maybe using some google API, but I don't remember.
Any insight on this? I haven't developed any extensions so I don't really know how they work.
[EDIT]
So I have been asked to show some code. As seen in my previous question ( window.onresize not firing in Chrome but firing in Chrome Incognito ), the problem occurs on any window.onresize event function, so I don't think my code really matters.
Also, there is quite a lot of my code, I don't know how much of it to paste or if it would be helpful.
var debounce = function (func, threshold, execAsap)
{
var timeout;
return function debounced () {//alert("1.1 Y U NO WORK?");
var obj = this, args = arguments;
function delayed () {
if (!execAsap)
func.apply(obj, args);
timeout = null;
}
if (timeout)
clearTimeout(timeout);
else if (execAsap)
func.apply(obj, args);
timeout = setTimeout(delayed, threshold || 100);
};
};
window.onresize = debounce(function (e) { //alert("1.2 Y U NO WORK?");
flag = true;
var point = window.center({width:1,height:1});
doCenter(point);
// does something here, but only once after mouse cursor stops
}, 100, false);
I would like to stress that the problem is not due to the debounce. window.onresize = t; function t (e) { alert("wtf?");} won't work either.
[EDIT2]
Here's the result:
var screenCapture = null;
var screenCaptureImg = document.createElement("img");
screenCaptureImg.setAttribute("src", "chrome-extension://cpngackimfmofbokmjmljamhdncknpmg/images/arrow.png");
/*
* Add event listeners for both "load"- and "error"-event
* Set the variable showing the existence of the extension by
* setting it to "true" or "false" according to the fired event
*/
screenCaptureImg.addEventListener("load", doLoad, false);
function doLoad(e){
screenCapture = true; //removeImgTag(e);
alert("I've so cleverly detected that your Chrome has the ScreenCapture extension enabled. \n\nThis extension interferes with my website's DOM and long story short, it won't be able to scale properly.\n\nSo please disable it. \nConsider this extension: \"Disable All Extensions Plus\", it's a handy selective disabler.");
}
screenCaptureImg.addEventListener("error", function(e){
screenCapture = false; //removeImgTag(e);
}, false);
/*
function removeImgTag(e) {
e.currentTarget.parentNode.removeChild(e.currentTarget);
}
*/
Note that I couldn't get removeImgTag to work, because (at least in chrome), I don't seem to have access to the document object in order to create or remove elements from my page, from within these event functions. This is also why I'm displaying an alert instead of elegantly writing up a document.getElementById("something").innerHTML=...
To detect if an extension is installed in Chrome, you can check for a known resource included in the extension such as an image. Resources for the extension are referenced using the following URL pattern:
chrome-extension://<extensionID>/<pathToFile>
The basic detection technique involves creating a hidden image tag and attaching load and error events to it to see if the image loads (as described here for Firefox):
extensionImg.setAttribute("src", "chrome-extension://<INSERT EXTENSION ID HERE>/images/someImage.png"); // See below for discussion of how to find this
/*
* Add event listeners for both "load"- and "error"-event
* Set the variable showing the existence of the extension by
* setting it to "true" or "false" according to the fired event
*/
extensionImg.addEventListener("load", function(e) {
extensionExists = true;
removeImgTag(e);
}, false);
extensionImg.addEventListener("error", function(e) {
extensionExists = false;
removeImgTag(e);
}, false);
function removeImgTag(e) {
e.currentTarget.parentNode.removeChild(e.currentTarget);
}
Check the installation directory of the extension in the Chrome configuration to find a likely target for detection. On my Linux workstation extensions are located in:
~/.config/chromium/Default/Extensions
You can see that I have 3 extensions installed right now:
~/.config/chromium/Default/Extensions$ ls
cpecbmjeidppdiampimghndkikcmoadk nmpeeekfhbmikbdhlpjbfmnpgcbeggic
cpngackimfmofbokmjmljamhdncknpmg
The odd looking names are the unique IDs given to the extension when it is uploaded to the Chrome webstore. You can obtain the ID either from the webstore or by going to the Extensions tab (wrench -> Extensions) and hovering over the link to the extension in question, or "Screen Capture (by Google)" in this case (note the asterisked extension ID):
https://chrome.google.com/webstore/detail/**cpngackimfmofbokmjmljamhdncknpmg**
In the extension directory there will be one or more versions; you can ignore this. Within the version directory is the actual content of the extension:
~/.config/chromium/Default/Extensions/cpngackimfmofbokmjmljamhdncknpmg/5.0.3_0$ ls
account.js images page.js sina_microblog.js
ajax.js isLoad.js picasa.js site.js
background.html _locales plugin style.css
editor.js manifest.json popup.html ui.js
facebook.js notification.html sha1.js upload_ui.js
hotkey_storage.js oauth.js shortcut.js
hub.html options.html showimage.css
i18n_styles page_context.js showimage.html
In the case of the Screen Capture extension there are a number of images to use:
~/.config/chromium/Default/Extensions/cpngackimfmofbokmjmljamhdncknpmg/5.0.3_0/images$ ls
arrow.png icon_128.png icon_save.png print.png
copy.png icon_16.png line.png region.png
cross.png icon_19.png loading.gif screen.png
custom.png icon_32.png loading_icon.gif sina_icon.png
delete_account_icon.png icon_48.png mark.png toolbar_bg.png
down_arrow.png icon_close.png picasa_icon.png upload.png
facebook_icon.png icon_copy.png popup_bg.jpg whole.png
These can be referenced under this URL:
chrome-extension://cpngackimfmofbokmjmljamhdncknpmg/images/arrow.png
This technique obviously depends on the stability of the content of the extension. I recommend using an image that looks likely to remain through all versions.
As mentioned above, the same technique can be used to detect Firefox extensions. In this case the content URL looks like this:
chrome://<EXTENSION NAME>/content/<PATH TO RESOURCE>
On my Linux workstation Firefox extensions are located in:
~/.mozilla/firefox/<USER PROFILE ID>/extensions
Where <USER PROFILE ID> looks something like this: "h4aqaewq.default"
You can see that I have 2 extensions installed right now, one of which is a directory installation and the other of which is a XPI (pronounced "zippy") file:
~/.mozilla/firefox/h4aqaewq.default/extensions$ ls
{3e9a3920-1b27-11da-8cd6-0800200c9a66} staged
firebug#software.joehewitt.com.xpi
The "staged" directory is where Firefox keeps extensions that will be updated (I think). The GUID directory with the brackets is a directory-based extension installation, and the .xpi file is Firebug.
Note: XPI is going away (see the link above). It's basically a zip file that can be opened and inspected by anything that understands zip. I used Emacs.
Finding the extension ID in Firefox is a bit more involved. Go to "Tools -> Add-ons", click the Extensions tab, click the "More" link next to the extension description, then click the "reviews" link to go to the Firefox extension site and get the ID from the URL (note the asterisked extension ID):
https://addons.mozilla.org/en-US/firefox/addon/**firebug**/reviews/?src=api
There's probably an easier way to do this; suggestions welcome.
TODO: how to find a likely image in a Firefox extension.
As an extra note, in Chrome you can only communicate with an extension via the shared DOM of the page: Host page communication