summaryrefslogtreecommitdiffstats
path: root/src/js/contentscripts/fingerprinting.js
diff options
context:
space:
mode:
Diffstat (limited to 'src/js/contentscripts/fingerprinting.js')
-rw-r--r--src/js/contentscripts/fingerprinting.js367
1 files changed, 367 insertions, 0 deletions
diff --git a/src/js/contentscripts/fingerprinting.js b/src/js/contentscripts/fingerprinting.js
new file mode 100644
index 0000000..0891896
--- /dev/null
+++ b/src/js/contentscripts/fingerprinting.js
@@ -0,0 +1,367 @@
+/*
+ * This file is part of Privacy Badger <https://www.eff.org/privacybadger>
+ * Copyright (C) 2015 Electronic Frontier Foundation
+ *
+ * Derived from Chameleon <https://github.com/ghostwords/chameleon>
+ * Copyright (C) 2015 ghostwords
+ *
+ * Privacy Badger is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * Privacy Badger is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Privacy Badger. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+function getFpPageScript() {
+
+ // code below is not a content script: no chrome.* APIs /////////////////////
+
+ // return a string
+ return "(" + function (DOCUMENT, dispatchEvent, CUSTOM_EVENT, ERROR, DATE, setTimeout, OBJECT) {
+
+ const V8_STACK_TRACE_API = !!(ERROR && ERROR.captureStackTrace);
+
+ if (V8_STACK_TRACE_API) {
+ ERROR.stackTraceLimit = Infinity; // collect all frames
+ } else {
+ // from https://github.com/csnover/TraceKit/blob/b76ad786f84ed0c94701c83d8963458a8da54d57/tracekit.js#L641
+ var geckoCallSiteRe = /^\s*(.*?)(?:\((.*?)\))?@?((?:file|https?|chrome):.*?):(\d+)(?::(\d+))?\s*$/i;
+ }
+
+ var event_id = DOCUMENT.currentScript.getAttribute('data-event-id');
+
+ // from Underscore v1.6.0
+ function debounce(func, wait, immediate) {
+ var timeout, args, context, timestamp, result;
+
+ var later = function () {
+ var last = DATE.now() - timestamp;
+ if (last < wait) {
+ timeout = setTimeout(later, wait - last);
+ } else {
+ timeout = null;
+ if (!immediate) {
+ result = func.apply(context, args);
+ context = args = null;
+ }
+ }
+ };
+
+ return function () {
+ context = this; // eslint-disable-line consistent-this
+ args = arguments;
+ timestamp = DATE.now();
+ var callNow = immediate && !timeout;
+ if (!timeout) {
+ timeout = setTimeout(later, wait);
+ }
+ if (callNow) {
+ result = func.apply(context, args);
+ context = args = null;
+ }
+
+ return result;
+ };
+ }
+
+ // messages the injected script
+ var send = (function () {
+ var messages = [];
+
+ // debounce sending queued messages
+ var _send = debounce(function () {
+ dispatchEvent.call(DOCUMENT, new CUSTOM_EVENT(event_id, {
+ detail: messages
+ }));
+
+ // clear the queue
+ messages = [];
+ }, 100);
+
+ return function (msg) {
+ // queue the message
+ messages.push(msg);
+
+ _send();
+ };
+ }());
+
+ /**
+ * Gets the stack trace by throwing and catching an exception.
+ * @returns {*} Returns the stack trace
+ */
+ function getStackTraceFirefox() {
+ let stack;
+
+ try {
+ throw new ERROR();
+ } catch (err) {
+ stack = err.stack;
+ }
+
+ return stack.split('\n');
+ }
+
+ /**
+ * Gets the stack trace using the V8 stack trace API:
+ * https://github.com/v8/v8/wiki/Stack-Trace-API
+ * @returns {*} Returns the stack trace
+ */
+ function getStackTrace() {
+ let err = {},
+ origFormatter,
+ stack;
+
+ origFormatter = ERROR.prepareStackTrace;
+ ERROR.prepareStackTrace = function (_, structuredStackTrace) {
+ return structuredStackTrace;
+ };
+
+ ERROR.captureStackTrace(err, getStackTrace);
+ stack = err.stack;
+
+ ERROR.prepareStackTrace = origFormatter;
+
+ return stack;
+ }
+
+ /**
+ * Strip away the line and column number (from stack trace urls)
+ * @param script_url The stack trace url to strip
+ * @returns {String} the pure URL
+ */
+ function stripLineAndColumnNumbers(script_url) {
+ return script_url.replace(/:\d+:\d+$/, '');
+ }
+
+ /**
+ * Parses the stack trace for the originating script URL
+ * without using the V8 stack trace API.
+ * @returns {String} The URL of the originating script
+ */
+ function getOriginatingScriptUrlFirefox() {
+ let trace = getStackTraceFirefox();
+
+ if (trace.length < 4) {
+ return '';
+ }
+
+ // this script is at 0, 1 and 2
+ let callSite = trace[3];
+
+ let scriptUrlMatches = callSite.match(geckoCallSiteRe);
+ return scriptUrlMatches && scriptUrlMatches[3] || '';
+ }
+
+ /**
+ * Parses the stack trace for the originating script URL.
+ * @returns {String} The URL of the originating script
+ */
+ function getOriginatingScriptUrl() {
+ let trace = getStackTrace();
+
+ if (OBJECT.prototype.toString.call(trace) == '[object String]') {
+ // we failed to get a structured stack trace
+ trace = trace.split('\n');
+ // this script is at 0, 1, 2 and 3
+ let script_url_matches = trace[4].match(/\((http.*:\d+:\d+)/);
+ // TODO do we need stripLineAndColumnNumbers (in both places) here?
+ return script_url_matches && stripLineAndColumnNumbers(script_url_matches[1]) || stripLineAndColumnNumbers(trace[4]);
+ }
+
+ if (trace.length < 2) {
+ return '';
+ }
+
+ // this script is at 0 and 1
+ let callSite = trace[2];
+
+ if (callSite.isEval()) {
+ // argh, getEvalOrigin returns a string ...
+ let eval_origin = callSite.getEvalOrigin(),
+ script_url_matches = eval_origin.match(/\((http.*:\d+:\d+)/);
+
+ // TODO do we need stripLineAndColumnNumbers (in both places) here?
+ return script_url_matches && stripLineAndColumnNumbers(script_url_matches[1]) || stripLineAndColumnNumbers(eval_origin);
+ } else {
+ return callSite.getFileName();
+ }
+ }
+
+ /**
+ * Monitor the writes in a canvas instance
+ * @param item special item objects
+ */
+ function trapInstanceMethod(item) {
+ var is_canvas_write = (
+ item.propName == 'fillText' || item.propName == 'strokeText'
+ );
+
+ item.obj[item.propName] = (function (orig) {
+ // set to true after the first write, if the method is not
+ // restorable. Happens if another library also overwrites
+ // this method.
+ var skip_monitoring = false;
+
+ function wrapped() {
+ var args = arguments;
+
+ if (is_canvas_write) {
+ // to avoid false positives,
+ // bail if the text being written is too short,
+ // of if we've already sent a monitoring payload
+ if (skip_monitoring || !args[0] || args[0].length < 5) {
+ return orig.apply(this, args);
+ }
+ }
+
+ var script_url = (
+ V8_STACK_TRACE_API ?
+ getOriginatingScriptUrl() :
+ getOriginatingScriptUrlFirefox()
+ ),
+ msg = {
+ obj: item.objName,
+ prop: item.propName,
+ scriptUrl: script_url
+ };
+
+ if (item.hasOwnProperty('extra')) {
+ msg.extra = item.extra.apply(this, args);
+ }
+
+ send(msg);
+
+ if (is_canvas_write) {
+ // optimization: one canvas write is enough,
+ // restore original write method
+ // to this CanvasRenderingContext2D object instance
+ // Careful! Only restorable if we haven't already been replaced
+ // by another lib, such as the hidpi polyfill
+ if (this[item.propName] === wrapped) {
+ this[item.propName] = orig;
+ } else {
+ skip_monitoring = true;
+ }
+ }
+
+ return orig.apply(this, args);
+ }
+
+ OBJECT.defineProperty(wrapped, "name", { value: orig.name });
+ OBJECT.defineProperty(wrapped, "length", { value: orig.length });
+ OBJECT.defineProperty(wrapped, "toString", { value: orig.toString.bind(orig) });
+
+ return wrapped;
+
+ }(item.obj[item.propName]));
+ }
+
+ var methods = [];
+
+ ['getImageData', 'fillText', 'strokeText'].forEach(function (method) {
+ var item = {
+ objName: 'CanvasRenderingContext2D.prototype',
+ propName: method,
+ obj: CanvasRenderingContext2D.prototype,
+ extra: function () {
+ return {
+ canvas: true
+ };
+ }
+ };
+
+ if (method == 'getImageData') {
+ item.extra = function () {
+ var args = arguments,
+ width = args[2],
+ height = args[3];
+
+ // "this" is a CanvasRenderingContext2D object
+ if (width === undefined) {
+ width = this.canvas.width;
+ }
+ if (height === undefined) {
+ height = this.canvas.height;
+ }
+
+ return {
+ canvas: true,
+ width: width,
+ height: height
+ };
+ };
+ }
+
+ methods.push(item);
+ });
+
+ methods.push({
+ objName: 'HTMLCanvasElement.prototype',
+ propName: 'toDataURL',
+ obj: HTMLCanvasElement.prototype,
+ extra: function () {
+ // "this" is a canvas element
+ return {
+ canvas: true,
+ width: this.width,
+ height: this.height
+ };
+ }
+ });
+
+ methods.forEach(trapInstanceMethod);
+
+ // save locally to keep from getting overwritten by site code
+ } + "(document, document.dispatchEvent, CustomEvent, Error, Date, setTimeout, Object));";
+
+ // code above is not a content script: no chrome.* APIs /////////////////////
+
+}
+
+// END FUNCTION DEFINITIONS ///////////////////////////////////////////////////
+
+(function () {
+
+// don't inject into non-HTML documents (such as XML documents)
+// but do inject into XHTML documents
+if (document instanceof HTMLDocument === false && (
+ document instanceof XMLDocument === false ||
+ document.createElement('div') instanceof HTMLDivElement === false
+)) {
+ return;
+}
+
+// TODO race condition; fix waiting on https://crbug.com/478183
+chrome.runtime.sendMessage({
+ type: "detectFingerprinting"
+}, function (enabled) {
+ if (!enabled) {
+ return;
+ }
+ /**
+ * Communicating to webrequest.js
+ */
+ var event_id = Math.random();
+
+ // listen for messages from the script we are about to insert
+ document.addEventListener(event_id, function (e) {
+ // pass these on to the background page
+ chrome.runtime.sendMessage({
+ type: "fpReport",
+ data: e.detail
+ });
+ });
+
+ window.injectScript(getFpPageScript(), {
+ event_id: event_id
+ });
+});
+
+}());