Path: blob/master/sites/bitcoin/fingerprinting.js
777 views
/*1* This file is part of Privacy Badger <https://www.eff.org/privacybadger>2* Copyright (C) 2015 Electronic Frontier Foundation3*4* Derived from Chameleon <https://github.com/ghostwords/chameleon>5* Copyright (C) 2015 ghostwords6*7* Privacy Badger is free software: you can redistribute it and/or modify8* it under the terms of the GNU General Public License version 3 as9* published by the Free Software Foundation.10*11* Privacy Badger is distributed in the hope that it will be useful,12* but WITHOUT ANY WARRANTY; without even the implied warranty of13* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the14* GNU General Public License for more details.15*16* You should have received a copy of the GNU General Public License17* along with Privacy Badger. If not, see <http://www.gnu.org/licenses/>.18*/1920function getFpPageScript() {2122// code below is not a content script: no chrome.* APIs /////////////////////2324// return a string25return "(" + function (ERROR) {2627const V8_STACK_TRACE_API = !!(ERROR && ERROR.captureStackTrace);2829if (V8_STACK_TRACE_API) {30ERROR.stackTraceLimit = Infinity; // collect all frames31} else {32// from https://github.com/csnover/TraceKit/blob/b76ad786f84ed0c94701c83d8963458a8da54d57/tracekit.js#L64133var geckoCallSiteRe = /^\s*(.*?)(?:\((.*?)\))?@?((?:file|https?|chrome):.*?):(\d+)(?::(\d+))?\s*$/i;34}3536var event_id = document.currentScript.getAttribute('data-event-id');3738// from Underscore v1.6.039function debounce(func, wait, immediate) {40var timeout, args, context, timestamp, result;4142var later = function () {43var last = Date.now() - timestamp;44if (last < wait) {45timeout = setTimeout(later, wait - last);46} else {47timeout = null;48if (!immediate) {49result = func.apply(context, args);50context = args = null;51}52}53};5455return function () {56context = this; // eslint-disable-line consistent-this57args = arguments;58timestamp = Date.now();59var callNow = immediate && !timeout;60if (!timeout) {61timeout = setTimeout(later, wait);62}63if (callNow) {64result = func.apply(context, args);65context = args = null;66}6768return result;69};70}7172// messages the injected script73var send = (function () {74var messages = [];7576// debounce sending queued messages77var _send = debounce(function () {78document.dispatchEvent(new CustomEvent(event_id, {79detail: messages80}));8182// clear the queue83messages = [];84}, 100);8586return function (msg) {87// queue the message88messages.push(msg);8990_send();91};92}());9394/**95* Gets the stack trace by throwing and catching an exception.96* @returns {*} Returns the stack trace97*/98function getStackTraceFirefox() {99let stack;100101try {102throw new Error();103} catch (err) {104stack = err.stack;105}106107return stack.split('\n');108}109110/**111* Gets the stack trace using the V8 stack trace API:112* https://github.com/v8/v8/wiki/Stack-Trace-API113* @returns {*} Returns the stack trace114*/115function getStackTrace() {116let err = {},117origFormatter,118stack;119120origFormatter = ERROR.prepareStackTrace;121ERROR.prepareStackTrace = function (_, structuredStackTrace) {122return structuredStackTrace;123};124125ERROR.captureStackTrace(err, getStackTrace);126stack = err.stack;127128ERROR.prepareStackTrace = origFormatter;129130return stack;131}132133/**134* Strip away the line and column number (from stack trace urls)135* @param script_url The stack trace url to strip136* @returns {String} the pure URL137*/138function stripLineAndColumnNumbers(script_url) {139return script_url.replace(/:\d+:\d+$/, '');140}141142/**143* Parses the stack trace for the originating script URL144* without using the V8 stack trace API.145* @returns {String} The URL of the originating script146*/147function getOriginatingScriptUrlFirefox() {148let trace = getStackTraceFirefox();149150if (trace.length < 4) {151return '';152}153154// this script is at 0, 1 and 2155let callSite = trace[3];156157let scriptUrlMatches = callSite.match(geckoCallSiteRe);158return scriptUrlMatches && scriptUrlMatches[3] || '';159}160161/**162* Parses the stack trace for the originating script URL.163* @returns {String} The URL of the originating script164*/165function getOriginatingScriptUrl() {166let trace = getStackTrace();167168if (trace.length < 2) {169return '';170}171172// this script is at 0 and 1173let callSite = trace[2];174175if (callSite.isEval()) {176// argh, getEvalOrigin returns a string ...177let eval_origin = callSite.getEvalOrigin(),178script_url_matches = eval_origin.match(/\((http.*:\d+:\d+)/);179180// TODO do we need stripLineAndColumnNumbers (in both places) here?181return script_url_matches && stripLineAndColumnNumbers(script_url_matches[1]) || stripLineAndColumnNumbers(eval_origin);182} else {183return callSite.getFileName();184}185}186187/**188* Monitor the writes in a canvas instance189* @param item special item objects190*/191function trapInstanceMethod(item) {192var is_canvas_write = (193item.propName == 'fillText' || item.propName == 'strokeText'194);195196item.obj[item.propName] = (function (orig) {197198return function () {199var args = arguments;200201if (is_canvas_write) {202// to avoid false positives,203// bail if the text being written is too short204if (!args[0] || args[0].length < 5) {205return orig.apply(this, args);206}207}208209var script_url = (210V8_STACK_TRACE_API ?211getOriginatingScriptUrl() :212getOriginatingScriptUrlFirefox()213),214msg = {215obj: item.objName,216prop: item.propName,217scriptUrl: script_url218};219220if (item.hasOwnProperty('extra')) {221msg.extra = item.extra.apply(this, args);222}223224send(msg);225226if (is_canvas_write) {227// optimization: one canvas write is enough,228// restore original write method229// to this CanvasRenderingContext2D object instance230this[item.propName] = orig;231}232233return orig.apply(this, args);234};235236}(item.obj[item.propName]));237}238239var methods = [];240241['getImageData', 'fillText', 'strokeText'].forEach(function (method) {242var item = {243objName: 'CanvasRenderingContext2D.prototype',244propName: method,245obj: CanvasRenderingContext2D.prototype,246extra: function () {247return {248canvas: true249};250}251};252253if (method == 'getImageData') {254item.extra = function () {255var args = arguments,256width = args[2],257height = args[3];258259// "this" is a CanvasRenderingContext2D object260if (width === undefined) {261width = this.canvas.width;262}263if (height === undefined) {264height = this.canvas.height;265}266267return {268canvas: true,269width: width,270height: height271};272};273}274275methods.push(item);276});277278methods.push({279objName: 'HTMLCanvasElement.prototype',280propName: 'toDataURL',281obj: HTMLCanvasElement.prototype,282extra: function () {283// "this" is a canvas element284return {285canvas: true,286width: this.width,287height: this.height288};289}290});291292methods.forEach(trapInstanceMethod);293294// save locally to keep from getting overwritten by site code295} + "(Error));";296297// code above is not a content script: no chrome.* APIs /////////////////////298299}300301/**302* Executes a script in the page DOM context303*304* @param text The content of the script to insert305* @param data attributes to set in the inserted script tag306*/307function insertFpScript(text, data) {308var parent = document.documentElement,309script = document.createElement('script');310311script.text = text;312script.async = false;313314for (var key in data) {315script.setAttribute('data-' + key.replace('_', '-'), data[key]);316}317318parent.insertBefore(script, parent.firstChild);319parent.removeChild(script);320}321322323// END FUNCTION DEFINITIONS ///////////////////////////////////////////////////324325(function () {326327// don't inject into non-HTML documents (such as XML documents)328// but do inject into XHTML documents329if (document instanceof HTMLDocument === false && (330document instanceof XMLDocument === false ||331document.createElement('div') instanceof HTMLDivElement === false332)) {333return;334}335336// TODO race condition; fix waiting on https://crbug.com/478183337chrome.runtime.sendMessage({checkEnabled: true},338function (enabled) {339if (!enabled) {340return;341}342/**343* Communicating to webrequest.js344*/345var event_id = Math.random();346347// listen for messages from the script we are about to insert348document.addEventListener(event_id, function (e) {349// pass these on to the background page350chrome.runtime.sendMessage({351'fpReport': e.detail352});353});354355insertFpScript(getFpPageScript(), {356event_id: event_id357});358}359);360361}());362363364