Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

"sandbox" the performance bookmarklet #47

Open
MorrisJohns opened this issue Apr 29, 2019 · 2 comments
Open

"sandbox" the performance bookmarklet #47

MorrisJohns opened this issue Apr 29, 2019 · 2 comments
Assignees

Comments

@MorrisJohns
Copy link

I have modified the performance bookmarklet so that it runs within an iframe with it's own security sandbox (so that the code is only loaded as needed, and so I don't need to audit your code much!). The performance data is passed from the "host" document to the "guest" iframe using sendMessage.

I have attached the code here to give an idea of the changes, although it would need a few hours work for anyone to use.

The function to show the performance metrics (e.g. connected to a button), run from the "host" document is:

function showTiming() {
	function objectify(object) {
		return JSON.parse(JSON.stringify(object));
	}
	var performance = window.performance;
	if (!performance || !performance.getEntriesByType) return;
	var perf = {
		resources: objectify(performance.getEntriesByType('resource')),
		marks: objectify(performance.getEntriesByType('mark')),
		measures: objectify(performance.getEntriesByType('measure')),
		timing: objectify(performance.timing),
		navigation: objectify(performance.navigation)
	};

	var html = '<script src="https://some.domain.here/performance-bookmarklet.js"></script>';
	var secondBody = document.createElement('body');
	secondBody.innerHTML = '<iframe class=nobox frameborder=0 src="about:blank"></iframe>';
	var iframe = secondBody.firstChild;
	if ('srcdoc' in iframe) {
		iframe.setAttribute('sandbox', 'allow-scripts');
		iframe.setAttribute('referrerpolicy', 'no-referrer');
		iframe.srcdoc = html;
	} else {	// srcdoc not IE but will be introduced in Edge 18
		setTimeout(function() {	// IE needs iframe to be in document so use postpone
			var iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
			iframeDoc.open();
			iframeDoc.write(html);
			iframeDoc.close();
		}, 0);
	}
	iframe.style.cssText = 'position:absolute;top:0;left:0;width:100%;height:100%;z-index:10000;';
	iframe.onload = function() {
		iframe.contentWindow.postMessage(perf, '*');
		iframe.onload = null;
	};
	var closeButton = document.createElement('button');
	closeButton.textContent = 'close';
	closeButton.style.cssText = 'position:fixed;top:20px;left:20px;z-index:10001;background-color:#BBF;';
	closeButton.onclick = function() {
		document.body.style.overflow = '';
		document.documentElement.removeChild(secondBody);
	}
	secondBody.appendChild(closeButton);
	document.body.style.overflow = 'hidden';
	document.documentElement.appendChild(secondBody);	// looks odd, but html spec does allow multiple body elements
}

I edited the compiled performance-bookmarklet.js (Yes this is a very dirty technique. Done because I didn't want to edit source code as then I would need to run your build process which would have taken me more time and I needed the code to work with our custom build process).

We put the following code around the performance-bookmarklet.js code (dirty: monkey patches window.performance to read the local data)

(function () {
	function go(perf) {
		window.performance = {
			timing: perf.timing,
			navigation: perf.navigation,
			getEntriesByType: function(type){
				if (type == "resource") {
					return perf.resources;
				} else if (type == 'mark') {
					return perf.marks;
				} else if (type == "measure") {
					return perf.measures;
				}
			}
		};
		execBookmarklet(withinIframe);
		window.performance = null;
	}

	if (window.doubleLoad) {
		return;
	}
	window.doubleLoad = 1;

	var withinIframe = window.top !== window.self;
	if (withinIframe) {
		window.onmessage = function(event) {
			var perf = event.data;
			if (perf.resources) {
				go(perf);
			}
		};
	} else {	// Called by 'Timing Graph Debug'
		var objectify = function(object) {
			return JSON.parse(JSON.stringify(object));
		}
		go({
			resources: objectify(window.performance.getEntriesByType("resource")),	// JSON.parse(JSON.stringify(window.performance.getEntriesByType("resource")))
			marks: objectify(window.performance.getEntriesByType("mark")),
			measures: objectify(window.performance.getEntriesByType("measure")),
			timing: objectify(window.performance.timing),
			navigation: objectify(window.performance.navigation)
		});
	}

	// put copyright into a string stored on an object so that the copyright isn't removed during compression
	execBookmarklet.copyright = 'Copyright 2014 Michael Mrowetz. MIT License: https://github.com/micmro/performance-bookmarklet/blob/d3e037620270419521eab20bf479c51c8ea72d96/LICENSE';

function execBookmarklet(withinIframe) {
// INSERT BOOKMARKLET CODE HERE
}
})();

The BOOKMARKLET CODE itself also has a few modifications.

Don't read localStorage (causes exception):

if (!withinIframe)
	var persistanceEnabled = !!JSON.parse(localStorage.getItem(storageKey));

Change the IFrameHolder code:

iFrameHolder.getOutputIFrame = function () {
	return outputIFrame;
};

module.exports = iFrameHolder;

if (withinIframe) {
	module.exports = {
		setup: function (onIFrameReady) {
			function addComponent(domEl) {
				document.body.appendChild(domEl);
			}
			var styleTag = dom.newTag("style", {
				type: "text/css",
				text: style
			});
			document.head.appendChild(styleTag);
			var outputHolder = dom.newTag("div", { id: "perfbook-holder" });
			var outputContent = dom.newTag("div", { id: "perfbook-content" });
			outputHolder.appendChild(outputContent);
			document.body.appendChild(outputHolder);
			onIFrameReady(addComponent);
		},
		getOutputIFrame: function(){
			return document;
		}
	};
}

Add connection details (very important for HTTP2 connection versus HTTP1.1 connection):

	var connection = navigator.connection;
	if (connection) {
		createAppendixDefValue(appendix, dom.newTag("abbr", { title: "Connection", text: "navigator.connection.*" }), "");
		for (var key in connection) {
			var value = connection[key];
			if (value && typeof value != "function") {
				createAppendixDefValue(appendix, dom.newTextNode("\xa0\xa0" + key + ":"), value);
			}
		}
	}

	tilesHolder.appendChild(appendix);
	return tilesHolder;

Dont check for about: protocol:

if (withinIframe) {
	if (!data.isValid()) {
		return;
	}
} else {
	if (location.protocol === "about:" || !data.isValid()) {
		return;
	}
}

Commented out dead code:

		// window.outputContent;

The final change was to remove the negative right margins (replaced -18px and -72px with 0 in the css style).

I have written this issue so that if anyone else wants to use your code others can branch and build on these snippets. Hopefully it is useful to someone so maybe just leave the issue open?!

I really really appreciate your code - it still works beautifully (2019).

@MorrisJohns
Copy link
Author

Uploaded edited performance-bookmarklet.js.txt
with the changes made. Compare changes against the old version of performanceBookmarklet.js it was based on: https://github.com/micmro/performance-bookmarklet/blob/36cc6ee1afcfaf1a1a85933affe317e2afb18738/dist/performanceBookmarklet.js

@micmro
Copy link
Owner

micmro commented May 1, 2019

Hi @MorrisJohns
thanks a lot for your work.
I will give it a proper review and update the code accordingly, but because I am moving countries in the moment I will unfortunately need another few weeks before I will get the time for this.

@micmro micmro self-assigned this Jun 29, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants