diff --git a/_locales/en/messages.json b/_locales/en/messages.json index be1d332..4ecee21 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -196,6 +196,12 @@ }, + "settingsStrictBlockingInfo": { + "message": "

Strict\nblocking,\nintroduced in version 0.3.6,\nmeans that even\nif you whitelist a specific hostname, blacklisted type of requests (plugins,\nframes, etc.) will remain blacklisted. For some users this maybe\ntoo bothersome, hence this switch.

\n

Strict blocking on: blacklisted types of request (if any)\nfor a whitelisted hostname are blocked (unless you explicitly whitelist\nspecifically these types of request for the whitelisted hostname.)

\n

Strict blocking off: blacklisted types of request (if any)\nfor a whitelisted hostname are allowed (unless you explicitly blacklist\nspecifically these types of request for the whitelisted hostname.)

", + "description": "" + }, + + "dummy": { "message": "This entry must be the last one", "description": "so we dont need to deal with comma for last entry" diff --git a/_locales/fr/messages.json b/_locales/fr/messages.json index 5539cb1..0e58e89 100644 --- a/_locales/fr/messages.json +++ b/_locales/fr/messages.json @@ -4,7 +4,7 @@ "description": "extension name." }, "extShortDesc": { - "message": "Simplement un clic pour interdire ou permettre n'importe quel types de requête faites par votre fureteur. Bloquez les scripts, iframes, pubs, facebook, etc.", + "message": "Un simple clic pour interdire ou permettre n'importe quel types de requête faites par votre fureteur. Bloquez les scripts, iframes, pubs, facebook, etc.", "description": "this will in chrome web store" }, "ruleManagerPageName": { @@ -196,6 +196,12 @@ }, + "settingsStrictBlockingInfo": { + "message": "Blocage strict, introduit dans la version 0.3.6, signifie que même si vous allouez un nom d'hôte spécifique, une requête restera bloquée si le type d'objet à requérir (plugins, iframes, etc) est lui-même bloqué. Pour certains utilisateurs, ce peut-être trop restrictif, d'où la possibilité de choisir la logique de blocage ici.\nBlocage strict: pour qu'une requête ne soit pas bloquée, le nom d'hôte ET le type de requête doivent être alloués.\nBlocage non strict: pour qu'une requête ne soit pas bloquée, seul le nom d'hôte doir être alloué.", + "description": "" + }, + + "dummy": { "message": "This entry must be the last one", "description": "so we dont need to deal with comma for last entry" diff --git a/assets/httpsb-blacklist.txt b/assets/httpsb-blacklist.txt index 53157df..eda9bc3 100644 --- a/assets/httpsb-blacklist.txt +++ b/assets/httpsb-blacklist.txt @@ -7,6 +7,7 @@ adgear.com # "AdGear is an online advertising technologies company base adnxs.com # "Adnxs.com is run by AppNexus, a company that provides technology, data and analytics to help companies buy and sell online display advertising" (Ref.: http://www.theguardian.com/technology/2012/apr/23/adnxs-tracking-trackers-cookies-web-monitoring) adobetag.com # "Adobe Announces Adobe Tag Manager for the Online Marketing Suite" aimatch.com # "Ad Server, SAS® Intelligent Advertising for Publishers" +analytics.edgesuite.net atedra.com # "Atedra est un réseau de publicité Internet francophone au Canada" axf8.net # https://www.eff.org/deeplinks/2013/06/third-party-resources-nsa-leaks betrad.com # "Evidon: Home | Online Marketing Intelligence, Web Analytics, Privacy" (which also publishes "Ghostery" add-on..) @@ -24,6 +25,8 @@ crsspxl.com # Related to crosspixel.net displaymarketplace.com erovinmo.com # No info whatsoever from site itself can be found = naughty corner. Ironically spotted at "http://www.technologyreview.com/news/519336/bruce-schneier-nsa-spying-is-making-us-less-safe/" (also: http://www.mywot.com/en/scorecard/erovinmo.com) exelator.com # "domain used by eXelate Media which is an advertising company that is part of a network of sites, cookies, and other technologies used to track you" (Ref.: http://www.donottrackplus.com/trackers/exelator.com.php) +everestjs.net # related to `everesttech.net` +everesttech.net # "search engine marketing (SEM) solutions", pixel image on the page, looks like tracking to me. Spotted @ `http://www.homedepot.ca/` (search worked fine when blocking this hostname) eyereturn.com # "eyeReturn Marketing is the only end-to-end digital advertising platform in the market" gigya.com # "The tools you need to connect with consumers, harness rich data, and make marketing relevant" krxd.net # https://www.eff.org/deeplinks/2013/06/third-party-resources-nsa-leaks diff --git a/background.html b/background.html index ee3a80a..24a91e0 100644 --- a/background.html +++ b/background.html @@ -8,7 +8,9 @@ + + diff --git a/info.html b/info.html index c20904e..d897d37 100644 --- a/info.html +++ b/info.html @@ -94,7 +94,7 @@

-

+

For Pages  diff --git a/js/async.js b/js/async.js index 51b31e9..0a5ef1e 100644 --- a/js/async.js +++ b/js/async.js @@ -188,11 +188,7 @@ function onMessageHandler(request, sender, callback) { case 'userSettings': if ( typeof request.name === 'string' && request.name !== '' ) { - if ( HTTPSB.userSettings[request.name] !== undefined && request.value !== undefined ) { - HTTPSB.userSettings[request.name] = request.value; - } - response = HTTPSB.userSettings[request.name]; - saveUserSettings(); + response = changeUserSettings(request.name, request.value); } break; @@ -212,7 +208,7 @@ function onMessageHandler(request, sender, callback) { } } - if ( callback ) { + if ( response && callback ) { callback(response); } } diff --git a/js/background.js b/js/background.js index 802de91..c3ff50b 100644 --- a/js/background.js +++ b/js/background.js @@ -32,7 +32,10 @@ var HTTPSB = { displayTextSize: '13px', popupHideBlacklisted: false, popupCollapseDomains: false, - popupCollapseSpecificDomains: {} + popupCollapseSpecificDomains: {}, + maxLoggedRequests: 250, + statsFilters: { + } }, runtimeId: 1, @@ -60,8 +63,11 @@ var HTTPSB = { // tabs are used to redirect stats collection to a specific url stats // structure. - pageUrlToTabId: { }, - tabIdToPageUrl: { }, + pageUrlToTabId: {}, + tabIdToPageUrl: {}, + + // Power switch to disengage HTTPSB + off: false, // page url => permission scope temporaryScopes: null, @@ -69,7 +75,7 @@ var HTTPSB = { // Current entries from remote blacklists -- // just hostnames, '*/' is implied, this saves significantly on memory. - blacklistReadonly: { }, + blacklistReadonly: {}, // https://github.com/gorhill/httpswitchboard/issues/19 excludeRegex: /^https?:\/\/chrome\.google\.com\/(extensions|webstore)/, diff --git a/js/cookies.js b/js/cookies.js index a0d23a9..c566147 100644 --- a/js/cookies.js +++ b/js/cookies.js @@ -34,12 +34,13 @@ var cookieHunter = { // rhill 2013-10-19: pageStats could be nil, for example, this can // happens if a file:// ... makes an xmlHttpRequest if ( pageStats ) { - this.queuePageRecord[pageUrlFromPageStats(pageStats)] = pageStats; + var pageURL = pageUrlFromPageStats(pageStats); + cookieHunter.queuePageRecord[pageURL] = pageStats; asyncJobQueue.add( 'cookieHunterPageRecord', null, function() { cookieHunter.processPageRecord(); }, - 500, + 1000, false); } }, @@ -51,7 +52,8 @@ var cookieHunter = { // rhill 2013-10-19: pageStats could be nil, for example, this can // happens if a file:// ... makes an xmlHttpRequest if ( pageStats ) { - this.queuePageRemove[pageUrlFromPageStats(pageStats)] = pageStats; + var pageURL = pageUrlFromPageStats(pageStats); + cookieHunter.queuePageRemove[pageURL] = pageStats; asyncJobQueue.add( 'cookieHunterPageRemove', null, @@ -63,40 +65,38 @@ var cookieHunter = { // Candidate for removal remove: function(cookie) { - this.queueRemove[cookie.url + '|' + cookie.name] = cookie; + cookieHunter.queueRemove[cookie.url + '|' + cookie.name] = cookie; }, processPageRecord: function() { // record cookies from a specific page - var pageUrls = Object.keys(this.queuePageRecord); + var pageUrls = Object.keys(cookieHunter.queuePageRecord); var i = pageUrls.length; while ( i-- ) { - this._processPageRecord(pageUrls[i]); + cookieHunter._processPageRecord(pageUrls[i]); } }, _processPageRecord: function(pageUrl) { - var me = this; chrome.cookies.getAll({}, function(cookies) { - me._hunt(me.queuePageRecord[pageUrl], cookies, true); - delete me.queuePageRecord[pageUrl]; + cookieHunter._hunt(cookieHunter.queuePageRecord[pageUrl], cookies, true); + delete cookieHunter.queuePageRecord[pageUrl]; }); }, processPageRemove: function() { // erase cookies from a specific page - var pageUrls = Object.keys(this.queuePageRemove); + var pageUrls = Object.keys(cookieHunter.queuePageRemove); var i = pageUrls.length; while ( i-- ) { - this._processPageRemove(pageUrls[i]); + cookieHunter._processPageRemove(pageUrls[i]); } }, _processPageRemove: function(pageUrl) { - var me = this; chrome.cookies.getAll({}, function(cookies) { - me._hunt(me.queuePageRemove[pageUrl], cookies, false); - delete me.queuePageRemove[pageUrl]; + cookieHunter._hunt(cookieHunter.queuePageRemove[pageUrl], cookies, false); + delete cookieHunter.queuePageRemove[pageUrl]; }); }, @@ -108,7 +108,6 @@ var cookieHunter = { if ( !httpsb.userSettings.deleteCookies ) { return; } - var me = this; chrome.cookies.getAll({}, function(cookies) { // quickProfiler.start(); var i = cookies.length; @@ -121,7 +120,7 @@ var cookieHunter = { cookieUrl = (cookie.secure ? 'https://' : 'http://') + domain + cookie.path; // be mindful of https://github.com/gorhill/httpswitchboard/issues/19 if ( !httpsb.excludeRegex.test(cookieUrl) ) { - me.remove({ + cookieHunter.remove({ url: cookieUrl, domain: cookie.domain, name: cookie.name @@ -139,14 +138,14 @@ var cookieHunter = { // Remove only some of the cookies which are candidate for removal: // who knows, maybe a user has 1000s of cookies sitting in his // browser... - var cookieKeys = Object.keys(this.queueRemove); + var cookieKeys = Object.keys(cookieHunter.queueRemove); if ( cookieKeys.length > 50 ) { cookieKeys = cookieKeys.sort(function(){return Math.random() < Math.random();}).splice(0, 50); } var cookieKey, cookie; while ( cookieKey = cookieKeys.pop() ) { - cookie = this.queueRemove[cookieKey]; - delete this.queueRemove[cookieKey]; + cookie = cookieHunter.queueRemove[cookieKey]; + delete cookieHunter.queueRemove[cookieKey]; // Just in case setting was changed after cookie was put in queue. if ( !httpsb.userSettings.deleteCookies ) { continue; @@ -154,7 +153,7 @@ var cookieHunter = { // Ensure cookie is not allowed on ALL current web pages: It can // happen that a cookie is blacklisted on one web page while // being whitelisted on another (because of per-page permissions). - if ( this._dontRemoveCookie(cookie) ) { + if ( cookieHunter._dontRemoveCookie(cookie) ) { // console.debug('HTTP Switchboard > cookieHunter.processRemove(): Will NOT remove cookie %s/{%s}', cookie.url, cookie.name); continue; } @@ -204,7 +203,7 @@ var cookieHunter = { // Leave alone cookies from behind-the-scene requests if // behind-the-scene processing is disabled. if ( block && deleteCookies && (pageUrl !== httpsb.behindTheSceneURL || httpsb.userSettings.processBehindTheSceneRequests) ) { - this.remove({ + cookieHunter.remove({ url: rootUrl + cookie.path, domain: cookie.domain, name: cookie.name diff --git a/js/httpsb.js b/js/httpsb.js index 8512332..6e0319a 100644 --- a/js/httpsb.js +++ b/js/httpsb.js @@ -131,6 +131,9 @@ HTTPSB.scopePageExists = function(url) { /******************************************************************************/ HTTPSB.evaluate = function(src, type, hostname) { + if ( this.off ) { + return 'gpt'; + } return this.temporaryScopes.evaluate(src, type, hostname); }; diff --git a/js/info.js b/js/info.js index f3e1cf7..41ed13b 100644 --- a/js/info.js +++ b/js/info.js @@ -52,12 +52,12 @@ function requestDetails(url, type, when, blocked) { function updateRequestData() { var requests = []; var pageUrls = targetUrl === 'All' ? - Object.keys(gethttpsb().pageStats) : - [targetUrl]; + Object.keys(gethttpsb().pageStats) : + [targetUrl]; var iPageUrl, nPageUrls, pageUrl; var reqKeys, iReqKey, nReqKeys, reqKey; var pageStats, pageRequests; - var i, entry; + var pos, reqURL, reqType, entry; nPageUrls = pageUrls.length; for ( iPageUrl = 0; iPageUrl < nPageUrls; iPageUrl++ ) { @@ -68,18 +68,24 @@ function updateRequestData() { continue; } pageRequests = pageStats.requests; - reqKeys = Object.keys(pageRequests); + reqKeys = pageRequests.getLoggedRequests(); nReqKeys = reqKeys.length; for ( iReqKey = 0; iReqKey < nReqKeys; iReqKey++ ) { reqKey = reqKeys[iReqKey]; - entry = pageRequests[reqKey]; - i = reqKey.indexOf('#'); - // Using parseFloat because of - // http://jsperf.com/performance-of-parseint + if ( !reqKey ) { + continue; + } + pos = reqKey.indexOf('#'); + reqURL = reqKey.slice(0, pos); + reqType = reqKey.slice(pos + 1); + entry = pageRequests.getLoggedRequestEntry(reqURL, reqType); + if ( !entry ) { + continue; + } requests.push(new requestDetails( - reqKey.slice(0, i), + reqURL, entry.when, - reqKey.slice(i+1), + reqType, entry.blocked )); } @@ -205,7 +211,8 @@ function renderStats() { '#allowedScriptCount': allowedStats.script, '#allowedXHRCount': allowedStats.xmlhttprequest, '#allowedSubFrameCount': allowedStats.sub_frame, - '#allowedOtherCount': allowedStats.other + '#allowedOtherCount': allowedStats.other, + '#maxLoggedRequests': httpsb.userSettings.maxLoggedRequests }); } @@ -240,7 +247,7 @@ function renderRequestRow(row, request) { $(cells[3]).text(request.url); } -/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ +/*----------------------------------------------------------------------------*/ function renderRequests() { var table = $('#requestsTable tbody'); @@ -275,6 +282,27 @@ function renderRequests() { /******************************************************************************/ +function changeFilterHandler() { + // Save new state of filters in user settings + // Initialize request filters as per user settings: + // https://github.com/gorhill/httpswitchboard/issues/49 + var statsFilters = gethttpsb().userSettings.statsFilters; + $('input[id^="show-"][type="checkbox"]').each(function() { + var input = $(this); + statsFilters[input.attr('id')] = !!input.prop('checked'); + }); + + chrome.runtime.sendMessage({ + what: 'userSettings', + name: 'statsFilters', + value: statsFilters + }); + + syncWithFilters(); +} + +/******************************************************************************/ + // Synchronize list of net requests with filter states function syncWithFilters() { @@ -322,9 +350,18 @@ function initAll() { $('#version').html(gethttpsb().manifest.version); $('a:not([target])').prop('target', '_blank'); + // Initialize request filters as per user settings: + // https://github.com/gorhill/httpswitchboard/issues/49 + $('input[id^="show-"][type="checkbox"]').each(function() { + var statsFilters = gethttpsb().userSettings.statsFilters; + var input = $(this); + var filter = statsFilters[input.attr('id')]; + input.prop('checked', filter === undefined || filter === true); + }); + // Event handlers $('#refresh-requests').on('click', renderRequests); - $('input[id^="show-"][type="checkbox"]').on('change', syncWithFilters); + $('input[id^="show-"][type="checkbox"]').on('change', changeFilterHandler); $('#selectPageUrls').on('change', targetUrlChangeHandler); renderTransientData(true); diff --git a/js/popup.js b/js/popup.js index c980f0b..8684c45 100644 --- a/js/popup.js +++ b/js/popup.js @@ -275,27 +275,27 @@ function initMatrixStats() { // collect all hostnames and ancestors from net traffic var background = getBackgroundPage(); + var uriTools = background.uriTools; var pageUrl = pageStats.pageUrl; - var url, hostname, reqType, nodes, node, reqKey; - var reqKeys = Object.keys(pageStats.requests); + var hostname, reqType, nodes, node, reqKey; + var reqKeys = pageStats.requests.getRequestKeys(); var iReqKeys = reqKeys.length; HTTPSBPopup.matrixHasRows = iReqKeys > 0; while ( iReqKeys-- ) { reqKey = reqKeys[iReqKeys]; - url = background.urlFromReqKey(reqKey); - hostname = background.uriTools.hostnameFromURI(url); + hostname = pageStats.requests.hostnameFromRequestKey(reqKey); // rhill 2013-10-23: hostname can be empty if the request is a data url // https://github.com/gorhill/httpswitchboard/issues/26 if ( hostname === '' ) { - hostname = background.uriTools.hostnameFromURI(pageUrl); + hostname = uriTools.hostnameFromURI(pageUrl); } - reqType = background.typeFromReqKey(reqKey); + reqType = pageStats.requests.typeFromRequestKey(reqKey); // we want a row for self and ancestors - nodes = background.uriTools.allHostnamesFromHostname(hostname); + nodes = uriTools.allHostnamesFromHostname(hostname); while ( true ) { node = nodes.shift(); @@ -1268,9 +1268,12 @@ function bindToTabHandler(tabs) { return; } - // Important! Before calling makeMenu() var background = getBackgroundPage(); var httpsb = getHTTPSB(); + + $('body').toggleClass('powerOff', httpsb.off); + + // Important! Before calling makeMenu() HTTPSBPopup.tabId = tabs[0].id; HTTPSBPopup.pageURL = background.pageUrlFromTabId(HTTPSBPopup.tabId); HTTPSBPopup.scopeURL = httpsb.normalizeScopeURL(HTTPSBPopup.pageURL); @@ -1301,6 +1304,22 @@ function bindToTabHandler(tabs) { /******************************************************************************/ +function togglePower(force) { + var httpsb = getHTTPSB(); + var off; + if ( typeof force === 'boolean' ) { + off = force; + } else { + off = !httpsb.off; + } + httpsb.off = off; + $('body').toggleClass('powerOff', off); + updateMatrixStats(); + updateMatrixColors(); +} + +/******************************************************************************/ + // make menu only when popup html is fully loaded function initAll() { @@ -1387,6 +1406,7 @@ function initAll() { $('#buttonRuleManager').text(chrome.i18n.getMessage('ruleManagerPageName')); $('#buttonInfo').text(chrome.i18n.getMessage('statsPageName')); $('#buttonSettings').text(chrome.i18n.getMessage('settingsPageName')); + $('#buttonPower').on('click', togglePower); $('#matList').on('click', '.g3Meta', function() { var separator = $(this); diff --git a/js/settings.js b/js/settings.js index 5694da1..4439e06 100644 --- a/js/settings.js +++ b/js/settings.js @@ -47,18 +47,22 @@ function initAll() { $('input[name="displayTextSize"]').attr('checked', function(){ return $(this).attr('value') === httpsb.userSettings.displayTextSize; }); + $('#strict-blocking').attr('checked', httpsb.userSettings.strictBlocking); $('#delete-blacklisted-cookies').attr('checked', httpsb.userSettings.deleteCookies); $('#delete-blacklisted-localstorage').attr('checked', httpsb.userSettings.deleteLocalStorage); $('#cookie-removed-counter').html(httpsb.cookieRemovedCounter); $('#localstorage-removed-counter').html(httpsb.localStorageRemovedCounter); $('#process-behind-the-scene').attr('checked', httpsb.userSettings.processBehindTheSceneRequests); - $('#strict-blocking').attr('checked', httpsb.userSettings.strictBlocking); + $('#max-logged-requests').val(httpsb.userSettings.maxLoggedRequests); // Handle user interaction $('input[name="displayTextSize"]').on('change', function(){ changeUserSettings('displayTextSize', $(this).attr('value')); }); + $('#strict-blocking').on('change', function(){ + changeUserSettings('strictBlocking', $(this).is(':checked')); + }); $('#delete-blacklisted-cookies').on('change', function(){ changeUserSettings('deleteCookies', $(this).is(':checked')); }); @@ -68,8 +72,19 @@ function initAll() { $('#process-behind-the-scene').on('change', function(){ changeUserSettings('processBehindTheSceneRequests', $(this).is(':checked')); }); - $('#strict-blocking').on('change', function(){ - changeUserSettings('strictBlocking', $(this).is(':checked')); + $('#max-logged-requests').on('change', function(){ + var oldVal = gethttpsb().userSettings.maxLoggedRequests; + var newVal = Math.round(parseFloat($(this).val())); + if ( typeof newVal !== 'number' ) { + newVal = oldVal; + } else { + newVal = Math.max(newVal, 0); + newVal = Math.min(newVal, 999); + } + $(this).val(newVal); + if ( newVal !== oldVal ) { + changeUserSettings('maxLoggedRequests', newVal); + } }); $('.whatisthis').on('click', function() { diff --git a/js/strpacker.js b/js/strpacker.js new file mode 100644 index 0000000..8a73efd --- /dev/null +++ b/js/strpacker.js @@ -0,0 +1,107 @@ +/******************************************************************************* + + httpswitchboard - a Chromium browser extension to black/white list requests. + Copyright (C) 2013 Raymond Hill + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program 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 this program. If not, see {http://www.gnu.org/licenses/}. + + Home: https://github.com/gorhill/httpswitchboard +*/ + +/******************************************************************************/ + +// It's just a dict-based "packer" + +var stringPacker = { + codeGenerator: 1, + codeJunkyard: [], + mapStringToEntry: {}, + mapCodeToString: {}, + base64Chars: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_', + + Entry: function(code) { + this.count = 0; + this.code = code; + }, + + remember: function(code) { + if ( code === '' ) { + return; + } + var s = this.mapCodeToString[code]; + if ( s ) { + var entry = this.mapStringToEntry[s]; + entry.count++; + } + }, + + forget: function(code) { + if ( code === '' ) { + return; + } + var s = this.mapCodeToString[code]; + if ( s ) { + var entry = this.mapStringToEntry[s]; + entry.count--; + if ( !entry.count ) { + // console.debug('stringPacker > releasing code "%s" (aka "%s")', code, s); + this.codeJunkyard.push(entry); + delete this.mapCodeToString[code]; + delete this.mapStringToEntry[s]; + } + } + }, + + pack: function(s) { + var entry = this.entryFromString(s); + if ( !entry ) { + return ''; + } + return entry.code; + }, + + unpack: function(packed) { + return this.mapCodeToString[packed] || ''; + }, + + base64: function(code) { + var s = ''; + var base64Chars = this.base64Chars; + while ( code ) { + s += String.fromCharCode(base64Chars.charCodeAt(code & 63)); + code >>>= 6; + } + return s; + }, + + entryFromString: function(s) { + if ( s === '' ) { + return null; + } + var entry = this.mapStringToEntry[s]; + if ( !entry ) { + entry = this.codeJunkyard.pop(); + if ( !entry ) { + entry = new this.Entry(this.base64(this.codeGenerator++)); + } else { + // console.debug('stringPacker > recycling code "%s" (aka "%s")', entry.code, s); + entry.count = 0; + } + this.mapStringToEntry[s] = entry; + this.mapCodeToString[entry.code] = s; + } + return entry; + } +}; + diff --git a/js/tab.js b/js/tab.js index 6218cdb..d47415c 100644 --- a/js/tab.js +++ b/js/tab.js @@ -21,29 +21,256 @@ /******************************************************************************/ -RequestStatsEntry.prototype.junkyard = []; +PageStatsRequestEntry.junkyard = []; -RequestStatsEntry.prototype.factory = function() { - var entry = RequestStatsEntry.prototype.junkyard.pop(); +/*----------------------------------------------------------------------------*/ + +PageStatsRequestEntry.factory = function() { + var entry = PageStatsRequestEntry.junkyard.pop(); if ( entry ) { return entry; } - return new RequestStatsEntry(); + return new PageStatsRequestEntry(); }; -RequestStatsEntry.prototype.dispose = function() { +/*----------------------------------------------------------------------------*/ + +PageStatsRequestEntry.prototype.dispose = function() { // Let's not grab and hold onto too much memory.. - if ( RequestStatsEntry.prototype.junkyard.length < 200 ) { - RequestStatsEntry.prototype.junkyard.push(this); + if ( PageStatsRequestEntry.junkyard.length < 200 ) { + PageStatsRequestEntry.junkyard.push(this); + } +}; + +/******************************************************************************/ + +PageStatsRequests.factory = function() { + var requests = new PageStatsRequests(); + requests.ringBuffer = new Array(HTTPSB.userSettings.maxLoggedRequests); + return requests; +}; + +/*----------------------------------------------------------------------------*/ + +// Request key: +// index: 01234567... +// HHHHHHTN... +// ^ ^^ +// | || +// | |+--- short string code for hostname (dict-based) +// | +--- single char code for type of request +// +--- FNV32a hash of whole URI (irreversible) + +PageStatsRequests.makeRequestKey = function(uri, reqType) { + // Ref: Given a URL, returns a unique 7-character long hash string + // Based on: FNV32a + // http://www.isthe.com/chongo/tech/comp/fnv/index.html#FNV-reference-source + // The rest is custom, suited for HTTPSB. + var hint = 0x811c9dc5; + var i = uri.length; + while ( i-- ) { + hint ^= uri.charCodeAt(i); + hint += hint<<1 + hint<<4 + hint<<7 + hint<<8 + hint<<24; + } + hint = hint >>> 0; + + // convert 32-bit hash to str + var hstr = ''; + i = 6; + while ( i-- ) { + hstr += PageStatsRequests.charCodes.charAt(hint & 0x3F); + hint >>= 6; + } + + // append code for type + hstr += PageStatsRequests.typeToCode[reqType] || 'z'; + + // append code for hostname + hstr += stringPacker.pack(uriTools.hostnameFromURI(uri)); + + return hstr; +}; + +PageStatsRequests.charCodes = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_'; +PageStatsRequests.typeToCode = { + 'main_frame' : 'a', + 'sub_frame' : 'b', + 'stylesheet' : 'c', + 'script' : 'd', + 'image' : 'e', + 'object' : 'f', + 'xmlhttprequest': 'g', + 'other' : 'h', + 'cookie' : 'i' +}; +PageStatsRequests.codeToType = { + 'a': 'main_frame', + 'b': 'sub_frame', + 'c': 'stylesheet', + 'd': 'script', + 'e': 'image', + 'f': 'object', + 'g': 'xmlhttprequest', + 'h': 'other', + 'i': 'cookie' +}; + +/*----------------------------------------------------------------------------*/ + +PageStatsRequests.rememberRequestKey = function(reqKey) { + stringPacker.remember(reqKey.slice(7)); +}; + +/*----------------------------------------------------------------------------*/ + +PageStatsRequests.forgetRequestKey = function(reqKey) { + stringPacker.forget(reqKey.slice(7)); +}; + +/*----------------------------------------------------------------------------*/ + +PageStatsRequests.hostnameFromRequestKey = function(reqKey) { + return stringPacker.unpack(reqKey.slice(7)); +}; +PageStatsRequests.prototype.hostnameFromRequestKey = PageStatsRequests.hostnameFromRequestKey; + +/*----------------------------------------------------------------------------*/ + +PageStatsRequests.typeFromRequestKey = function(reqKey) { + return PageStatsRequests.codeToType[reqKey.charAt(6)]; +}; +PageStatsRequests.prototype.typeFromRequestKey = PageStatsRequests.typeFromRequestKey; + +/*----------------------------------------------------------------------------*/ + +PageStatsRequests.prototype.createEntryIfNotExists = function(url, type, block) { + this.logRequest(url, type); + var reqKey = PageStatsRequests.makeRequestKey(url, type); + var entry = this.requests[reqKey]; + if ( entry ) { + entry.when = Date.now(); + entry.blocked = block; + return false; + } + PageStatsRequests.rememberRequestKey(reqKey); + entry = PageStatsRequestEntry.factory(); + entry.when = Date.now(); + entry.blocked = block; + this.requests[reqKey] = entry; + return true; +}; + +/*----------------------------------------------------------------------------*/ + +PageStatsRequests.prototype.resizeLogBuffer = function(size) { + if ( size === this.ringBuffer.length ) { + return; + } + if ( !size ) { + this.ringBuffer = new Array(0); + this.ringBufferPointer = 0; + return; + } + var newBuffer = new Array(size); + var copySize = Math.min(size, this.ringBuffer.length); + var newBufferPointer = (copySize % size) | 0; + var isrc = this.ringBufferPointer; + var ides = newBufferPointer; + while ( copySize-- ) { + isrc--; + if ( isrc < 0 ) { + isrc = this.ringBuffer.length - 1; + } + ides--; + if ( ides < 0 ) { + ides = size - 1; + } + newBuffer[ides] = this.ringBuffer[isrc]; + } + this.ringBuffer = newBuffer; + this.ringBufferPointer = newBufferPointer; +}; + +/*----------------------------------------------------------------------------*/ + +PageStatsRequests.prototype.logRequest = function(url, type) { + var buffer = this.ringBuffer; + var len = buffer.length; + if ( !len ) { + return; } + var pointer = this.ringBufferPointer; + buffer[pointer] = url + '#' + type; + this.ringBufferPointer = ((pointer + 1) % len) | 0; +}; + +/*----------------------------------------------------------------------------*/ + +PageStatsRequests.prototype.getLoggedRequests = function() { + var buffer = this.ringBuffer; + if ( !buffer.length ) { + return []; + } + // [0 - pointer] = most recent + // [pointer - length] = least recent + // thus, ascending order: + // [pointer - length] + [0 - pointer] + var pointer = this.ringBufferPointer; + return buffer.slice(pointer).concat(buffer.slice(0, pointer)).reverse(); +}; + +/*----------------------------------------------------------------------------*/ + +PageStatsRequests.prototype.getLoggedRequestEntry = function(reqURL, reqType) { + return this.requests[PageStatsRequests.makeRequestKey(reqURL, reqType)]; +}; + +/*----------------------------------------------------------------------------*/ + +PageStatsRequests.prototype.getRequestKeys = function() { + return Object.keys(this.requests); +}; + +/*----------------------------------------------------------------------------*/ + +PageStatsRequests.prototype.getEntry = function(reqKey) { + return this.requests[reqKey]; +}; + +/*----------------------------------------------------------------------------*/ + +PageStatsRequests.prototype.disposeOne = function(reqKey) { + if ( this.requests[reqKey] ) { + this.requests[reqKey].dispose(); + delete this.requests[reqKey]; + PageStatsRequests.forgetRequestKey(reqKey); + } +}; + +/*----------------------------------------------------------------------------*/ + +PageStatsRequests.prototype.dispose = function() { + var requests = this.requests; + for ( var reqKey in requests ) { + if ( requests.hasOwnProperty(reqKey) ) { + stringPacker.forget(reqKey.slice(7)); + requests[reqKey].dispose(); + delete requests[reqKey]; + } + } + var i = this.ringBuffer.length; + while ( i-- ) { + this.ringBuffer[i] = ''; + } + this.ringBufferPointer = 0; }; /******************************************************************************/ -PageStatsEntry.prototype.junkyard = []; +PageStatsEntry.junkyard = []; -PageStatsEntry.prototype.factory = function(pageUrl) { - var entry = PageStatsEntry.prototype.junkyard.pop(); +PageStatsEntry.factory = function(pageUrl) { + var entry = PageStatsEntry.junkyard.pop(); if ( entry ) { return entry.init(pageUrl); } @@ -54,7 +281,7 @@ PageStatsEntry.prototype.factory = function(pageUrl) { PageStatsEntry.prototype.init = function(pageUrl) { this.pageUrl = pageUrl; - this.requests = {}; + this.requests = PageStatsRequests.factory(); this.domains = {}; this.state = {}; this.requestStats.reset(); @@ -68,26 +295,17 @@ PageStatsEntry.prototype.init = function(pageUrl) { /******************************************************************************/ PageStatsEntry.prototype.dispose = function() { - // Iterate through all requests and return them to the junkyard for - // later reuse. - var reqKeys = Object.keys(this.requests); - var i = reqKeys.length; - var reqKey; - while ( i-- ) { - reqKey = reqKeys[i]; - this.requests[reqKey].dispose(); - delete this.requests[reqKey]; - } + this.requests.dispose(); + // rhill 2013-11-07: Even though at init time these are reset, I still // need to release the memory taken by these, which can amount to // sizeable enough chunks (especially requests, through the request URL // used as a key). this.pageUrl = ''; - this.requests = {}; this.domains = {}; this.state = {}; - PageStatsEntry.prototype.junkyard.push(this); + PageStatsEntry.junkyard.push(this); }; /******************************************************************************/ @@ -120,21 +338,11 @@ PageStatsEntry.prototype.recordRequest = function(type, url, block) { } else { this.perLoadAllowedRequestCount++; } - // var packedUrl = urlPacker.remember(url) + '#' + type; - var reqKey = url + '#' + type; - var requestStatsEntry = this.requests[reqKey]; - if ( requestStatsEntry ) { - requestStatsEntry.when = Date.now(); - requestStatsEntry.blocked = block; + if ( !this.requests.createEntryIfNotExists(url, type, block) ) { return; } - requestStatsEntry = RequestStatsEntry.prototype.factory(); - requestStatsEntry.when = Date.now(); - requestStatsEntry.blocked = block; - this.requests[reqKey] = requestStatsEntry; - this.distinctRequestCount++; this.domains[hostname] = true; @@ -160,6 +368,8 @@ PageStatsEntry.prototype.getPageURL = function() { // notifying me, and this causes internal cached state to be out of sync. PageStatsEntry.prototype.updateBadge = function(tabId) { + var httpsb = HTTPSB; + // Icon var iconPath; var total = this.perLoadAllowedRequestCount + this.perLoadBlockedRequestCount; @@ -173,27 +383,30 @@ PageStatsEntry.prototype.updateBadge = function(tabId) { } chrome.browserAction.setIcon({ tabId: tabId, path: iconPath }); - // Badge text - var count = this.distinctRequestCount; - var iconStr = count.toFixed(0); - if ( count >= 1000 ) { - if ( count < 10000 ) { - iconStr = iconStr.slice(0,1) + '.' + iconStr.slice(1,-2) + 'K'; - } else if ( count < 1000000 ) { - iconStr = iconStr.slice(0,-3) + 'K'; - } else if ( count < 10000000 ) { - iconStr = iconStr.slice(0,1) + '.' + iconStr.slice(1,-5) + 'M'; - } else { - iconStr = iconStr.slice(0,-6) + 'M'; + // Badge text & color + var badgeStr, badgeColor; + if ( httpsb.off ) { + badgeStr = '!!!'; + badgeColor = '#F00'; + } else { + var count = this.distinctRequestCount; + badgeStr = count.toFixed(0); + if ( count >= 1000 ) { + if ( count < 10000 ) { + badgeStr = badgeStr.slice(0,1) + '.' + badgeStr.slice(1,-2) + 'K'; + } else if ( count < 1000000 ) { + badgeStr = badgeStr.slice(0,-3) + 'K'; + } else if ( count < 10000000 ) { + badgeStr = badgeStr.slice(0,1) + '.' + badgeStr.slice(1,-5) + 'M'; + } else { + badgeStr = badgeStr.slice(0,-6) + 'M'; + } } + badgeColor = httpsb.scopePageExists(this.pageUrl) ? '#66F' : '#000'; } - chrome.browserAction.setBadgeText({ tabId: tabId, text: iconStr }); - // Badge color - chrome.browserAction.setBadgeBackgroundColor({ - tabId: tabId, - color: HTTPSB.scopePageExists(this.pageUrl) ? '#66F' : '#000' - }); + chrome.browserAction.setBadgeText({ tabId: tabId, text: badgeStr }); + chrome.browserAction.setBadgeBackgroundColor({ tabId: tabId, color: badgeColor }); }; /******************************************************************************/ @@ -242,8 +455,8 @@ function garbageCollectStalePageStatsCallback() { // Prune content of chromium-behind-the-scene virtual tab var pageStats = httpsb.pageStats[httpsb.behindTheSceneURL]; if ( pageStats ) { - var reqKeys = Object.keys(pageStats.requests); - if ( reqKeys > httpsb.behindTheSceneMaxReq ) { + var reqKeys = pageStats.requests.getRequestKeys(); + if ( reqKeys.length > httpsb.behindTheSceneMaxReq ) { reqKeys = reqKeys.sort(function(a,b){ var ra = pageStats.requests[a]; var rb = pageStats.requests[b]; @@ -252,11 +465,8 @@ function garbageCollectStalePageStatsCallback() { return 0; }).slice(httpsb.behindTheSceneMaxReq); var iReqKey = reqKeys.length; - var reqKey; while ( iReqKey-- ) { - reqKey = reqKeys[iReqKey]; - pageStats.requests[reqKey].dispose(); - delete pageStats.requests[reqKey]; + pageStats.requests.disposeOne(reqKeys[iReqKey]); } } } @@ -287,7 +497,7 @@ function createPageStats(pageUrl) { var httpsb = HTTPSB; var pageStats = httpsb.pageStats[pageUrl]; if ( !pageStats ) { - pageStats = PageStatsEntry.prototype.factory(pageUrl); + pageStats = PageStatsEntry.factory(pageUrl); httpsb.pageStats[pageUrl] = pageStats; } else if ( pageStats.pageUrl !== pageUrl ) { pageStats.init(pageUrl); @@ -454,18 +664,16 @@ function computeTabState(tabId) { // It is a critical error for a tab to not be defined here var httpsb = HTTPSB; var pageUrl = pageStats.pageUrl; - var reqKeys = Object.keys(pageStats.requests); + var reqKeys = pageStats.requests.getRequestKeys(); var i = reqKeys.length; var computedState = {}; - var url, domain, type; - var reqKey; + var hostname, type, reqKey; while ( i-- ) { reqKey = reqKeys[i]; - url = urlFromReqKey(reqKey); - domain = uriTools.hostnameFromURI(url); - type = typeFromReqKey(reqKey); - if ( httpsb.blacklisted(pageUrl, type, domain) ) { - computedState[type + '|' + domain] = true; + hostname = PageStatsRequests.hostnameFromRequestKey(reqKey); + type = PageStatsRequests.typeFromRequestKey(reqKey); + if ( httpsb.blacklisted(pageUrl, type, hostname) ) { + computedState[type + '|' + hostname] = true; } } return computedState; diff --git a/js/traffic.js b/js/traffic.js index bade023..5dbbb4d 100644 --- a/js/traffic.js +++ b/js/traffic.js @@ -99,7 +99,7 @@ background: #c00; \ // Intercept and filter web requests according to white and black lists. -function webRequestHandler(details) { +function beforeRequestHandler(details) { var httpsb = HTTPSB; var tabId = details.tabId; @@ -180,7 +180,7 @@ function webRequestHandler(details) { } } - // quickProfiler.stop('webRequestHandler | evaluate&record'); + // quickProfiler.stop('beforeRequestHandler | evaluate&record'); // If it is a frame and scripts are blacklisted for the // hostname, disable scripts for this hostname, necessary since inline @@ -197,7 +197,7 @@ function webRequestHandler(details) { // whitelisted? if ( !block ) { - // console.debug('webRequestHandler > allowing %s from %s', type, hostname); + // console.debug('beforeRequestHandler > allowing %s from %s', type, hostname); // If the request is not blocked, this means the response could contain // cookies. Thus, we go cookie hunting for this page url and record all @@ -206,17 +206,20 @@ function webRequestHandler(details) { // rhill 2013-11-07: Senseless to do this for behind-the-scene // requests. - if ( tabId !== httpsb.behindTheSceneTabId ) { + // rhill 2013-12-03: Do this here only for root frames. This is also + // done in `onHeadersReceived` when a `Set-cookie` directive is + // received. + if ( isRootFrame && tabId !== httpsb.behindTheSceneTabId ) { cookieHunter.record(pageStats); } - // quickProfiler.stop('webRequestHandler'); + // quickProfiler.stop('beforeRequestHandler'); // console.log("HTTPSB > %s @ url=%s", details.type, details.url); return; } // blacklisted - // console.debug('webRequestHandler > blocking %s from %s', type, hostname); + // console.debug('beforeRequestHandler > blocking %s from %s', type, hostname); // If it's a blacklisted frame, redirect to frame.html // rhill 2013-11-05: The root frame contains a link to noop.css, this @@ -240,7 +243,7 @@ function webRequestHandler(details) { return { "redirectUrl": dataURI }; } - // quickProfiler.stop('webRequestHandler'); + // quickProfiler.stop('beforeRequestHandler'); return { "cancel": true }; } @@ -249,7 +252,7 @@ function webRequestHandler(details) { // This is to handle cookies leaving the browser. -function webHeaderRequestHandler(details) { +function beforeSendHeadersHandler(details) { // Ignore traffic outside tabs if ( details.tabId < 0 ) { @@ -260,7 +263,7 @@ function webHeaderRequestHandler(details) { var hostname = uriTools.hostnameFromURI(details.url); var blacklistCookie = HTTPSB.blacklisted(pageUrlFromTabId(details.tabId), 'cookie', hostname); var headers = details.requestHeaders; - var i = details.requestHeaders.length; + var i = headers.length; while ( i-- ) { if ( headers[i].name.toLowerCase() !== 'cookie' ) { continue; @@ -272,7 +275,35 @@ function webHeaderRequestHandler(details) { } if ( blacklistCookie ) { - return { requestHeaders: details.requestHeaders }; + return { requestHeaders: headers }; + } +} + +/******************************************************************************/ + +// This is to handle cookies arriving in the browser. + +function headersReceivedHandler(details) { + + // Ignore traffic outside tabs + var tabId = details.tabId; + if ( tabId < 0 || tabId === HTTPSB.behindTheSceneTabId ) { + return; + } + + var pageStats = pageStatsFromTabId(tabId); + if ( !pageStats ) { + return; + } + + // Any `set-cookie` directive in there? + var headers = details.responseHeaders; + var i = headers.length; + while ( i-- ) { + if ( headers[i].name.toLowerCase() === 'set-cookie' ) { + cookieHunter.record(pageStats); + break; + } } } @@ -294,7 +325,7 @@ function startWebRequestHandler(from) { } chrome.webRequest.onBeforeRequest.addListener( - webRequestHandler, + beforeRequestHandler, { "urls": [ "http://*/*", @@ -316,7 +347,7 @@ function startWebRequestHandler(from) { ); chrome.webRequest.onBeforeSendHeaders.addListener( - webHeaderRequestHandler, + beforeSendHeadersHandler, { 'urls': [ "http://*/*", @@ -326,5 +357,16 @@ function startWebRequestHandler(from) { ['blocking', 'requestHeaders'] ); + chrome.webRequest.onHeadersReceived.addListener( + headersReceivedHandler, + { + 'urls': [ + "http://*/*", + "https://*/*" + ] + }, + ['responseHeaders'] + ); + HTTPSB.webRequestHandler = true; } diff --git a/js/types.js b/js/types.js index 6d8be85..6e1ccec 100644 --- a/js/types.js +++ b/js/types.js @@ -60,15 +60,20 @@ function PermissionScopes(httpsb) { this.scopes['*'] = new PermissionScope(httpsb); } -function RequestStatsEntry() { +function PageStatsRequestEntry() { this.when = 0; this.blocked = false; } +function PageStatsRequests() { + this.requests = {}; + this.ringBuffer = null; + this.ringBufferPointer = 0; +} + function PageStatsEntry(pageUrl) { this.pageUrl = ''; - this.requests = {}; - this.packedRequests = null; + this.requests = PageStatsRequests.factory(); this.domains = {}; this.state = {}; this.visible = false; @@ -77,6 +82,7 @@ function PageStatsEntry(pageUrl) { this.distinctRequestCount = 0; this.perLoadAllowedRequestCount = 0; this.perLoadBlockedRequestCount = 0; + this.off = false; this.init(pageUrl); } diff --git a/js/uritools.js b/js/uritools.js index 5cc8e52..dcf444b 100644 --- a/js/uritools.js +++ b/js/uritools.js @@ -153,7 +153,7 @@ var uriTools = { var pos = s.indexOf('@'); if ( pos >= 0 ) { - s = s.slice(0, pos+ 1); + s = s.slice(0, pos + 1); } // authority = host [ ":" port ] @@ -185,12 +185,8 @@ var uriTools = { /*--------------------------------------------------------------------*/ - fragment: function(fragment) { - if ( fragment === undefined ) { - return this._fragment; - } - this._fragment = fragment; - return this; + query: function() { + return this._query; }, /*--------------------------------------------------------------------*/ @@ -233,6 +229,35 @@ var uriTools = { /*--------------------------------------------------------------------*/ + directory: function() { + if ( this._hostname !== '' ) { + var pos = this._path.lastIndexOf('/'); + if ( pos === 0 ) { + return ''; + } + return this._path.slice(1, pos); + } + return ''; + }, + + /*--------------------------------------------------------------------*/ + + filename: function() { + if ( this._hostname !== '' ) { + var pos = this._path.lastIndexOf('/'); + return this._path.slice(pos + 1); + } + return this._path; + }, + + /*--------------------------------------------------------------------*/ + + fragment: function() { + return this._fragment; + }, + + /*--------------------------------------------------------------------*/ + // Normalize the way HTTPSB expects it normalizeURI: function(uri) { @@ -428,6 +453,7 @@ var uriTools = { }, /*--------------------------------------------------------------------*/ + _scheme: '', _hostname: '', _ipv4: undefined, @@ -473,5 +499,3 @@ var uriTools = { uriTools.authorityBit = (uriTools.userBit | uriTools.passwordBit | uriTools.hostnameBit | uriTools.portBit); uriTools.normalizeBits = (uriTools.schemeBit | uriTools.hostnameBit | uriTools.pathBit | uriTools.queryBit); -/******************************************************************************/ - diff --git a/js/urlpacker.js b/js/urlpacker.js index eab7b77..384bd5c 100644 --- a/js/urlpacker.js +++ b/js/urlpacker.js @@ -24,104 +24,181 @@ // Experimental function UrlPackerEntry(code) { - this.count = 1; + this.count = 0; this.code = code; } -var urlPacker = { - uri: new URI(), - codeGenerator: 0, - codeJunkyard: [], - fragmentToCode: {}, - codeToFragment: {}, - codeDigits: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_', - - remember: function(url) { - this.uri.href(url); - var scheme = this.uri.scheme(); - var hostname = this.uri.hostname(); - var directory = this.uri.directory(); - var leaf = this.uri.filename() + this.uri.search(); - var entry; - var packedScheme; - if ( scheme !== '' ) { - entry = this.fragmentToCode[scheme]; - if ( !entry ) { - entry = this.codeJunkyard.pop(); - packedScheme = this.strFromCode(this.codeGenerator++); - if ( !entry ) { - entry = new UrlPackerEntry(packedScheme); - } else { - entry.code = packedScheme; - entry.count = 1; - } - this.fragmentToCode[scheme] = entry; - this.codeToFragment[packedScheme] = scheme; - } else { - packedScheme = entry.code; - entry.count++; - } - } else { - packedScheme = ''; +var uriPacker = { + codeGenerator: 1, + codeJunkyard: [], // once "released", candidates for "recycling" + mapSegmentToCode: {}, + mapCodeToSegment: {}, + base64Chars: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_', + + remember: function(packedURL) { + // {scheme}/{hostname}/{directory}/filename?query#{fragment} + // {scheme} + var end = packedURL.indexOf('/'); + this.acquireCode(packedURL.slice(0, end)); + // {hostname} + var beg = end + 1; + end = packedURL.indexOf('/', beg); + this.acquireCode(packedURL.slice(beg, end)); + // {directory} + beg = end + 1; + end = packedURL.indexOf('/', beg); + this.acquireCode(packedURL.slice(beg, end)); + // {fragment} + beg = end + 1; + end = packedURL.indexOf('#', beg); + this.acquireCode(packedURL.slice(end + 1)); + }, + + forget: function(packedURL) { + // {scheme}/{hostname}/{directory}/filename?query#{fragment} + // {scheme} + var end = packedURL.indexOf('/'); + this.releaseCode(packedURL.slice(0, end)); + // {hostname} + var beg = end + 1; + end = packedURL.indexOf('/', beg); + this.releaseCode(packedURL.slice(beg, end)); + // {directory} + beg = end + 1; + end = packedURL.indexOf('/', beg); + this.releaseCode(packedURL.slice(beg, end)); + // {fragment} + beg = end + 1; + end = packedURL.indexOf('#', beg); + this.releaseCode(packedURL.slice(end + 1)); + }, + + pack: function(url) { + var ut = uriTools; + ut.uri(url); + return this.codeFromSegment(ut.scheme()) + '/' + + this.codeFromSegment(ut.hostname()) + '/' + + this.codeFromSegment(ut.directory()) + '/' + + ut.filename() + '?' + ut.query() + '#' + + this.codeFromSegment(ut.fragment()); + }, + + unpack: function(packedURL) { + // {scheme}/{hostname}/{directory}/filename?query#{fragment} + // {scheme} + var end = packedURL.indexOf('/'); + var uri = this.mapCodeToSegment[packedURL.slice(0, end)] + ':'; + // {hostname} + var beg = end + 1; + end = packedURL.indexOf('/', beg); + var segment = this.mapCodeToSegment[packedURL.slice(beg, end)]; + if ( segment ) { + uri += '//' + segment + '/'; } - var packedHostname; - if ( hostname !== '' ) { - entry = this.fragmentToCode[hostname]; - if ( !entry ) { - entry = this.codeJunkyard.pop(); - packedHostname = this.strFromCode(this.codeGenerator++); - if ( !entry ) { - entry = new UrlPackerEntry(packedHostname); - } else { - entry.code = packedHostname; - entry.count = 1; - } - this.fragmentToCode[hostname] = entry; - this.codeToFragment[packedHostname] = hostname; - } else { - packedHostname = entry.code; - entry.count++; - } - } else { - packedHostname = ''; + // {directory} + beg = end + 1; + end = packedURL.indexOf('/', beg); + segment = this.mapCodeToSegment[packedURL.slice(beg, end)]; + if ( segment ) { + uri += segment; } - var packedDirectory; - if ( directory !== '' ) { - entry = this.fragmentToCode[directory]; - if ( !entry ) { - packedDirectory = this.strFromCode(this.codeGenerator++); - entry = this.codeJunkyard.pop(); - if ( !entry ) { - entry = new UrlPackerEntry(packedDirectory); - } else { - entry.code = packedDirectory; - entry.count = 1; - } - this.fragmentToCode[directory] = entry; - this.codeToFragment[packedDirectory] = directory; - } else { - packedDirectory = entry.code; - entry.count++; - } - } else { - packedDirectory = ''; + // filename + beg = end + 1; + end = packedURL.indexOf('?', beg); + segment = packedURL.slice(beg, end); + if ( segment !== '' ) { + uri += segment; + } + // query + beg = end + 1; + end = packedURL.indexOf('#', beg); + segment = packedURL.slice(beg, end); + if ( segment !== '' ) { + uri += '?' + segment; } - // Return assembled packed fragments - return packedScheme + '/' + packedHostname + '/' + packedDirectory + '/' + leaf; + // {fragment} + beg = end + 1; + segment = this.mapCodeToSegment[packedURL.slice(beg)]; + if ( segment ) { + uri += '#' + segment; + } + return uri; }, - forget: function() { + unpackHostname: function(packedURL) { + // {scheme}/{hostname}/{directory}/filename?query#{fragment} + var beg = packedURL.indexOf('/') + 1; + var end = packedURL.indexOf('/', beg); + var code = packedURL.slice(beg, end); + if ( code ) { + return this.mapCodeToSegment[code]; + } + return ''; + }, + + unpackFragment: function(packedURL) { + // {scheme}/{hostname}/{directory}/filename?query#{fragment} + var beg = packedURL.lastIndexOf('#') + 1; + var code = packedURL.slice(beg); + if ( code ) { + return this.mapCodeToSegment[code]; + } + return ''; }, - strFromCode: function(code) { + base64: function(code) { var s = ''; - var codeDigits = this.codeDigits; + var base64Chars = this.base64Chars; while ( code ) { - s = s + String.fromCharCode(codeDigits.charCodeAt(code & 63)); - code = code >> 6; + s += String.fromCharCode(base64Chars.charCodeAt(code & 63)); + code >>>= 6; } return s; }, + codeFromSegment: function(segment) { + if ( segment === '' ) { + return ''; + } + var entry = this.mapSegmentToCode[segment]; + if ( !entry ) { + entry = this.codeJunkyard.pop(); + if ( !entry ) { + entry = new UrlPackerEntry(this.base64(this.codeGenerator++)); + } else { + console.debug('uriPacker > recycling code "%s" (aka "%s")', entry.code, segment); + entry.count = 0; + } + var code = entry.code; + this.mapSegmentToCode[segment] = entry; + this.mapCodeToSegment[code] = segment; + return code; + } + return entry.code; + }, + + acquireCode: function(code) { + if ( code === '' ) { + return; + } + var segment = this.mapCodeToSegment[code]; + var entry = this.mapSegmentToCode[segment]; + entry.count++; + }, + + releaseCode: function(code) { + if ( code === '' ) { + return; + } + var segment = this.mapCodeToSegment[code]; + var entry = this.mapSegmentToCode[segment]; + entry.count--; + if ( !entry.count ) { + console.debug('uriPacker > releasing code "%s" (aka "%s")', code, segment); + this.codeJunkyard.push(entry); + delete this.mapCodeToSegment[code]; + delete this.mapSegmentToCode[segment]; + } + } }; diff --git a/js/usersettings.js b/js/usersettings.js new file mode 100644 index 0000000..24c054e --- /dev/null +++ b/js/usersettings.js @@ -0,0 +1,59 @@ +/******************************************************************************* + + httpswitchboard - a Chromium browser extension to black/white list requests. + Copyright (C) 2013 Raymond Hill + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program 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 this program. If not, see {http://www.gnu.org/licenses/}. + + Home: https://github.com/gorhill/httpswitchboard +*/ + +/******************************************************************************/ + +function changeUserSettings(name, value) { + if ( typeof name !== 'string' || name === '' ) { + return; + } + + // Do not allow an unknown user setting to be created + if ( HTTPSB.userSettings[name] === undefined ) { + return; + } + + if ( value === undefined ) { + return HTTPSB.userSettings[name]; + } + + switch ( name ) { + + // Need to visit each pageStats object to resize ring buffer + case 'maxLoggedRequests': + value = Math.max(Math.min(value, 500), 0); + HTTPSB.userSettings[name] = value; + var pageStats = HTTPSB.pageStats; + for ( var pageUrl in pageStats ) { + if ( pageStats.hasOwnProperty(pageUrl) ) { + pageStats[pageUrl].requests.resizeLogBuffer(value); + } + } + break; + + default: + HTTPSB.userSettings[name] = value; + break; + } + + saveUserSettings(); +} + diff --git a/js/utils.js b/js/utils.js index 6991f6f..de55835 100644 --- a/js/utils.js +++ b/js/utils.js @@ -55,58 +55,3 @@ function setJavascript(hostname, state) { }); } -/******************************************************************************/ - -// Ref: Given a URL, returns a unique 7-character long hash string - -function requestHash(url, reqtype) { - - // FNV32a - // http://www.isthe.com/chongo/tech/comp/fnv/index.html#FNV-reference-source - var hint = 0x811c9dc5; - var i = s.length; - while ( i-- ) { - hint ^= s.charCodeAt(i); - hint += hint<<1 + hint<<4 + hint<<7 + hint<<8 + hint<<24; - } - hint = hint >>> 0; - - var hstr = requestHash.typeToCode[reqtype] || 'z'; - var i = 6; - while ( i-- ) { - hstr += requestHash.charCodes.charAt(hint & 0x3F); - hint >>= 6; - } - return hstr; -} - -requestHash.charCodes = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_'; - -requestHash.typeToCode = { - 'main_frame' : 'a', - 'sub_frame' : 'b', - 'stylesheet' : 'c', - 'script' : 'd', - 'image' : 'e', - 'object' : 'f', - 'xmlhttprequest': 'g', - 'other' : 'h', - 'cookie' : 'i' -}; -requestHash.codeToType = { - 'a': 'main_frame', - 'b': 'sub_frame', - 'c': 'stylesheet', - 'd': 'script', - 'e': 'image', - 'f': 'object', - 'g': 'xmlhttprequest', - 'h': 'other', - 'i': 'cookie', - 'z': 'unknown' -}; - -requestHash.typeFromHash = function(hstr) { - return requestHash.codeToType[hstr.charAt(0)]; -}; - diff --git a/manifest.json b/manifest.json index 3282cf4..260beae 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 2, "name": "__MSG_extName__", - "version": "0.6.1", + "version": "0.6.2", "description": "__MSG_extShortDesc__", "icons": { "128": "icon_128.png" diff --git a/popup.html b/popup.html index 8c448e6..78f9555 100644 --- a/popup.html +++ b/popup.html @@ -13,7 +13,7 @@ font: 13px httpsb,sans-serif; background-color: white; min-width: 32em; - min-height: 10em; + min-height: 15em; } body.scope-is-page { background-color: #f2f2ff; @@ -53,9 +53,15 @@ .scope-is-page #buttonToggleScope { background-image: url('img/remove-page-permissions.png'); } +body.powerOff #buttonToggleScope { + display: none; + } #buttonRevert { background-image: url('img/clear-temporary-rules.png'); } +body.powerOff #buttonRevert { + display: none; + } #buttonDropdown { background-image: url('img/dropdown.png'); } @@ -83,6 +89,9 @@ background-repeat: no-repeat; background-position: left 4px center; } +body.powerOff #buttonPower { + background-image: url('img/system-shutdown-symbolic-off.png'); +} .paneContent { padding-top: 5.5em; @@ -176,12 +185,18 @@ .matrix .g3Meta.g3Collapsed ~ .matSection { display: none; } +body.powerOff .matrix .g3Meta.g3Collapsed ~ .matSection { + display: block; + } .matrix .g3Meta ~ .matRow.ro { display: none; } .matrix .g3Meta.g3Collapsed ~ .matRow.ro { display: block; } +body.powerOff .matrix .g3Meta.g3Collapsed ~ .matRow.ro { + display: none; + } .matrix .matGroup .g3Meta + *,.matrix .matGroup .g3Meta + * + * { margin-top: 0; padding-top: 0; @@ -199,6 +214,10 @@ .matRow.rw .matCell { cursor: pointer; } +body.powerOff .matRow.rw .matCell { + cursor: auto; + opacity: 0.6; + } .rpt { color: black; background-color: #f8d0d0; @@ -283,6 +302,10 @@ .matCell.gdt #cellMenu { display: block; } +body.powerOff .matCell #cellMenu { + display: none; + } + #persist, #unpersist { margin: 1px; border: 0; @@ -344,6 +367,9 @@ #blacklist { top: 50%; } +body.powerOff #whitelist, body.powerOff #blacklist { + display: none; + } .rw .matCell.rpt #whitelist:hover { background-color: #080; opacity: 0.25; @@ -385,6 +411,7 @@ background: transparent url('img/domain-collapse.png') no-repeat; opacity: 0.25; z-index: 10000; + cursor: pointer; } .matSection.collapsed #domainOnly { background: transparent url('img/domain-expand.png') no-repeat; @@ -428,7 +455,7 @@
  • Statistics...
  • Settings...
  • -
  • On/off [nyi]
  • +
  • HTTPSB on/off
  • diff --git a/settings.html b/settings.html index c277fbe..c581609 100644 --- a/settings.html +++ b/settings.html @@ -80,19 +80,7 @@

    Strict blocking

    @@ -145,7 +133,25 @@

    Chromium: behind-the-scene requests

    + +

    Detailed requests log

    + +