diff --git a/platform/firefox/vapi-background.js b/platform/firefox/vapi-background.js index dd164b0..eb13d41 100644 --- a/platform/firefox/vapi-background.js +++ b/platform/firefox/vapi-background.js @@ -1971,7 +1971,7 @@ vAPI.cookies.getAll = function(callback) { vAPI.cookies.remove = function(details, callback) { // TODO if ( typeof callback === 'function' ) { - callback(); + callback(null); } }; /******************************************************************************/ diff --git a/src/css/logger-ui.css b/src/css/logger-ui.css index b6bdf77..c67bf16 100644 --- a/src/css/logger-ui.css +++ b/src/css/logger-ui.css @@ -1,6 +1,8 @@ body { + background-color: white; border: 0; box-sizing: border-box; + color: black; -moz-box-sizing: border-box; margin: 0; overflow-x: hidden; @@ -57,67 +59,62 @@ input:focus { } #content table tr { background-color: #fafafa; + color: #444; } #content table tr.cat_info { - color: #00a; + color: #00f; } #content table tr.blocked { - color: #c00; + color: #f00; } #content table tr:nth-of-type(2n+1) { background-color: #eee; } -#content table tr > td[colspan="3"]:nth-of-type(2) { - white-space: normal; - word-break: break-all; - word-wrap: break-word; - } #content table tr.doc { background-color: #666; color: white; text-align: center; } -#content table tr.doc > td { - border: 0; - } -#content table tr.doc > td:nth-of-type(2) { - padding: 0.6em 0; - } -/* -#content table tr.allowed { - background-color: rgba(0, 160, 0, 0.1); - } -#content table tr.allowed:nth-of-type(2n+1) { - background-color: rgba(0, 160, 0, 0.2); - } -body.colorBlind #content table tr.allowed { - background-color: rgba(255, 194, 57, 0.1) - } -*/ body:not(.filterOff) #content table tr.hidden { display: none; } #content table tr td { border: 1px solid #ccc; + min-width: 0.5em; padding: 3px; vertical-align: top; } +#content table tr.doc > td { + border: 0; + } #content table tr td:nth-of-type(1) { text-align: center; white-space: pre; width: 8em; } #content table tr td:nth-of-type(2) { - white-space: pre; - width: 2em; + width: 1em; } #content table tr td:nth-of-type(3) { white-space: pre; - width: 8em; + width: 2em; } #content table tr td:nth-of-type(4) { + white-space: pre; + width: 8em; + } +#content table tr td:nth-of-type(5) { border-right: none; white-space: normal; word-break: break-all; word-wrap: break-word; } +#content table tr.tab_bts > td:nth-of-type(2):before { + content: '\f070'; + font: 1em FontAwesome; + } +#content table tr > td[colspan="3"]:nth-of-type(3) { + white-space: normal; + word-break: break-all; + word-wrap: break-word; + } diff --git a/src/css/popup.css b/src/css/popup.css index 15619ea..780c00c 100644 --- a/src/css/popup.css +++ b/src/css/popup.css @@ -69,7 +69,7 @@ body .toolbar button.disabled { color: #ccc; } body .toolbar button.fa { - font: 1.75em 'FontAwesome'; + font: 1.75em FontAwesome; min-width: 1.1em; } body.tScopeGlobal .scopeRel:not(.disabled) { diff --git a/src/js/cookies.js b/src/js/cookies.js index 01097f6..50c3220 100644 --- a/src/js/cookies.js +++ b/src/js/cookies.js @@ -287,20 +287,28 @@ var removeCookieAsync = function(cookieKey) { /******************************************************************************/ +// TODO: i18n + var chromeCookieRemove = function(url, name) { + var sessionCookieKey = cookieKeyFromCookieURL(url, 'session', name); + var persistCookieKey = cookieKeyFromCookieURL(url, 'persistent', name); var callback = function(details) { - if ( !details ) { - return; + var success = !!details; + if ( cookieDict.hasOwnProperty(sessionCookieKey) ) { + if ( success ) { + µm.logger.writeOne('', 'info', 'cookie deleted: ' + sessionCookieKey); + µm.cookieRemovedCounter += 1; + } else { + µm.logger.writeOne('', 'error', 'failed to delete cookie: ' + sessionCookieKey); + } } - var cookieKey = cookieKeyFromCookieURL(details.url, 'session', details.name); - if ( removeCookieFromDict(cookieKey) ) { - µm.logger.writeOne('', 'info', 'cookie deleted: ' + cookieKey); - µm.cookieRemovedCounter += 1; - } - cookieKey = cookieKeyFromCookieURL(details.url, 'persistent', details.name); - if ( removeCookieFromDict(cookieKey) ) { - µm.logger.writeOne('', 'info', 'cookie deleted: ' + cookieKey); - µm.cookieRemovedCounter += 1; + if ( cookieDict.hasOwnProperty(persistCookieKey) ) { + if ( success ) { + µm.logger.writeOne('', 'info', 'cookie deleted: ' + persistCookieKey); + µm.cookieRemovedCounter += 1; + } else { + µm.logger.writeOne('', 'error', 'failed to delete cookie: ' + persistCookieKey); + } } }; diff --git a/src/js/logger-ui.js b/src/js/logger-ui.js index bca315f..e931785 100644 --- a/src/js/logger-ui.js +++ b/src/js/logger-ui.js @@ -41,8 +41,9 @@ var body = doc.body; var tbody = doc.querySelector('#content tbody'); var trJunkyard = []; var tdJunkyard = []; -var firstVarDataCol = 1; -var lastVarDataCol = 3; +var firstVarDataCol = 2; // currently, column 2 (0-based index) +var lastVarDataIndex = 3; // currently, d0-d3 +var noTabId = ''; var prettyRequestTypes = { 'main_frame': 'doc', @@ -61,12 +62,22 @@ var timeOptions = { /******************************************************************************/ -var createCell = function() { - var td = tdJunkyard.pop(); - if ( td ) { - return td; +var createCellAt = function(tr, index) { + var td = tr.cells[index]; + var mustAppend = !td; + if ( mustAppend ) { + td = tdJunkyard.pop(); } - return doc.createElement('td'); + if ( td ) { + td.removeAttribute('colspan'); + td.textContent = ''; + } else { + td = doc.createElement('td'); + } + if ( mustAppend ) { + tr.appendChild(td); + } + return td; }; /******************************************************************************/ @@ -78,23 +89,13 @@ var createRow = function(entry) { } else { tr = doc.createElement('tr'); } - var td; for ( var index = 0; index < firstVarDataCol; index++ ) { - td = tr.cells[index]; - if ( td === undefined ) { - td = createCell(); - tr.appendChild(td); - } - td.removeAttribute('colspan'); + createCellAt(tr, index); } - var i = 1, span = 1; + var i = 1, span = 1, td; for (;;) { - td = tr.cells[index]; - if ( td === undefined ) { - td = createCell(); - tr.appendChild(td); - } - if ( i === lastVarDataCol ) { + td = createCellAt(tr, index); + if ( i === lastVarDataIndex ) { break; } if ( entry['d' + i] === undefined ) { @@ -102,8 +103,6 @@ var createRow = function(entry) { } else { if ( span !== 1 ) { td.setAttribute('colspan', span); - } else { - td.removeAttribute('colspan'); } index += 1; span = 1; @@ -112,8 +111,6 @@ var createRow = function(entry) { } if ( span !== 1 ) { td.setAttribute('colspan', span); - } else { - td.removeAttribute('colspan'); } index += 1; while ( td = tr.cells[index] ) { @@ -134,17 +131,25 @@ var createGap = function(url) { /******************************************************************************/ var renderLogEntry = function(entry) { + var fvdc = firstVarDataCol; var tr = createRow(entry); - tr.classList.add('tab_' + entry.tab); - tr.classList.add('cat_' + entry.cat); + if ( entry.tab === noTabId ) { + tr.classList.add('tab_bts'); + } else if ( entry.tab !== '' ) { + tr.classList.add('tab_' + entry.tab); + } + if ( entry.cat !== '' ) { + tr.classList.add('cat_' + entry.cat); + } var time = new Date(entry.tstamp); tr.cells[0].textContent = time.toLocaleString('fullwide', timeOptions); switch ( entry.cat ) { + case 'error': case 'info': - tr.cells[firstVarDataCol].textContent = entry.d0; + tr.cells[fvdc].textContent = entry.d0; break; case 'net': @@ -155,15 +160,16 @@ var renderLogEntry = function(entry) { } if ( entry.d0 ) { tr.classList.add('blocked'); - tr.cells[1].textContent = '---'; + tr.cells[fvdc].textContent = '---'; } else { - tr.cells[1].textContent = ''; + tr.cells[fvdc].textContent = ''; } - tr.cells[2].textContent = (prettyRequestTypes[entry.d1] || entry.d1) + '\t'; - tr.cells[3].textContent = entry.d2 + '\t'; + tr.cells[fvdc+1].textContent = (prettyRequestTypes[entry.d1] || entry.d1) + '\t'; + tr.cells[fvdc+2].textContent = entry.d2 + '\t'; break; default: + tr.cells[fvdc].textContent = entry.d0; break; } @@ -178,6 +184,8 @@ var renderLogBuffer = function(response) { return; } + noTabId = response.noTabId; + // Preserve scroll position var height = tbody.offsetHeight; diff --git a/src/js/logger.js b/src/js/logger.js index 20e6164..e92a17e 100644 --- a/src/js/logger.js +++ b/src/js/logger.js @@ -64,7 +64,9 @@ LogEntry.prototype.init = function(args) { /******************************************************************************/ LogEntry.prototype.dispose = function() { - this.url = this.hostname = this.type = this.result = ''; + this.tstamp = 0; + this.tab = this.cat = ''; + this.d0 = this.d1 = this.d2 = this.d3 = undefined; if ( logEntryJunkyard.length < logEntryJunkyardMax ) { logEntryJunkyard.push(this); } @@ -152,16 +154,26 @@ var logBuffer = null; // thus removed from memory. var logBufferObsoleteAfter = 60 * 1000; -// The janitor will look for stale log buffer every 2 minutes. -var loggerJanitorPeriod = 2 * 60 * 1000; +/******************************************************************************/ + +var janitor = function() { + if ( + logBuffer !== null && + logBuffer.lastReadTime < (Date.now() - logBufferObsoleteAfter) + ) { + logBuffer = logBuffer.dispose(); + } + if ( logBuffer !== null ) { + setTimeout(janitor, logBufferObsoleteAfter); + } +}; /******************************************************************************/ var writeOne = function() { - if ( logBuffer === null ) { - return; + if ( logBuffer !== null ) { + logBuffer.writeOne(arguments); } - logBuffer.writeOne(arguments); }; /******************************************************************************/ @@ -169,26 +181,13 @@ var writeOne = function() { var readAll = function(tabId) { if ( logBuffer === null ) { logBuffer = new LogBuffer(); + setTimeout(janitor, logBufferObsoleteAfter); } return logBuffer.readAll(); }; /******************************************************************************/ -var loggerJanitor = function() { - if ( - logBuffer !== null && - logBuffer.lastReadTime < (Date.now() - logBufferObsoleteAfter) - ) { - logBuffer = logBuffer.dispose(); - } - setTimeout(loggerJanitor, loggerJanitorPeriod); -}; - -setTimeout(loggerJanitor, loggerJanitorPeriod); - -/******************************************************************************/ - return { writeOne: writeOne, readAll: readAll diff --git a/src/js/messaging.js b/src/js/messaging.js index 2bc0c83..7f6d9de 100644 --- a/src/js/messaging.js +++ b/src/js/messaging.js @@ -974,6 +974,7 @@ var onMessage = function(request, sender, callback) { case 'readMany': response = { colorBlind: false, + noTabId: vAPI.noTabId, entries: µm.logger.readAll(request.tabId) }; break; diff --git a/src/js/tab.js b/src/js/tab.js index 7d6e9cd..e36e5c3 100644 --- a/src/js/tab.js +++ b/src/js/tab.js @@ -157,8 +157,10 @@ housekeep itself. this.stack = []; this.rawURL = this.normalURL = + this.scheme = this.rootHostname = this.rootDomain = ''; + this.secure = false; this.timer = null; this.onTabCallback = null; this.onTimerCallback = null; @@ -209,13 +211,19 @@ housekeep itself. // root URL. TabContext.prototype.update = function() { if ( this.stack.length === 0 ) { - this.rawURL = this.normalURL = this.rootHostname = this.rootDomain = ''; + this.rawURL = + this.normalURL = + this.scheme = + this.rootHostname = + this.rootDomain = ''; } else { this.rawURL = this.stack[this.stack.length - 1]; this.normalURL = µm.normalizePageURL(this.tabId, this.rawURL); + this.scheme = µm.URI.schemeFromURI(this.rawURL); this.rootHostname = µm.URI.hostnameFromURI(this.normalURL); this.rootDomain = µm.URI.domainFromHostname(this.rootHostname) || this.rootHostname; } + this.secure = µm.URI.isSecureScheme(this.scheme); }; // Called whenever a candidate root URL is spotted for the tab. @@ -223,6 +231,10 @@ housekeep itself. if ( vAPI.isBehindTheSceneTabId(this.tabId) ) { return; } + var count = this.stack.length; + if ( count !== 0 && this.stack[count - 1] === url ) { + return; + } this.stack.push(url); this.update(); }; diff --git a/src/js/traffic.js b/src/js/traffic.js index c819cfc..2dd19c9 100644 --- a/src/js/traffic.js +++ b/src/js/traffic.js @@ -73,11 +73,10 @@ var onBeforeRootFrameRequestHandler = function(details) { var onBeforeRequestHandler = function(details) { var µm = µMatrix; - var µmuri = µm.URI.set(details.url); - var requestScheme = µmuri.scheme; // rhill 2014-02-17: Ignore 'filesystem:': this can happen when listening // to 'chrome-extension://'. + var requestScheme = µm.URI.schemeFromURI(details.url); if ( requestScheme === 'filesystem' ) { return; } @@ -96,7 +95,7 @@ var onBeforeRequestHandler = function(details) { var requestURL = details.url; // Ignore non-http schemes - if ( requestScheme.indexOf('http') !== 0 ) { + if ( requestScheme.lastIndexOf('http', 0) !== 0 ) { return; } @@ -115,10 +114,21 @@ var onBeforeRequestHandler = function(details) { // https://github.com/gorhill/httpswitchboard/issues/91#issuecomment-37180275 var tabContext = µm.tabContextManager.mustLookup(details.tabId); var tabId = tabContext.tabId; - var requestHostname = µmuri.hostname; + + // Enforce strict secure connection? + var block = false; + if ( + tabContext.secure && + µm.URI.isSecureScheme(requestScheme) === false && + µm.tMatrix.evaluateSwitchZ('https-strict', tabContext.rootHostname) + ) { + block = true; + } // Disallow request as per temporary matrix? - var block = µm.mustBlock(tabContext.rootHostname, requestHostname, requestType); + if ( block === false ) { + block = µm.mustBlock(tabContext.rootHostname, details.hostname, requestType); + } // Record request. // https://github.com/gorhill/httpswitchboard/issues/342 @@ -126,16 +136,16 @@ var onBeforeRequestHandler = function(details) { // processing has already been performed, and that a synthetic URL has // been constructed for logging purpose. Use this synthetic URL if // it is available. - var pageStore = µm.mustPageStoreFromTabId(details.tabId); + var pageStore = µm.mustPageStoreFromTabId(tabContext.tabId); pageStore.recordRequest(requestType, requestURL, block); - // whitelisted? + // Allowed? if ( !block ) { // console.debug('onBeforeRequestHandler()> ALLOW "%s": %o', details.url, details); return; } - // blacklisted + // Blocked // console.debug('onBeforeRequestHandler()> BLOCK "%s": %o', details.url, details); return { 'cancel': true }; @@ -198,16 +208,14 @@ var onBeforeSendHeadersHandler = function(details) { // If we reach this point, request is not blocked, so what is left to do // is to sanitize headers. - var reqHostname = µm.hostnameFromURL(requestURL); - - if ( µm.mustBlock(pageStore.pageHostname, reqHostname, 'cookie') ) { + if ( µm.mustBlock(pageStore.pageHostname, details.hostname, 'cookie') ) { if ( details.requestHeaders.setHeader('cookie', '') ) { µm.cookieHeaderFoiledCounter++; } } if ( µm.tMatrix.evaluateSwitchZ('referrer-spoof', pageStore.pageHostname) ) { - foilRefererHeaders(µm, reqHostname, details); + foilRefererHeaders(µm, details.hostname, details); } if ( µm.tMatrix.evaluateSwitchZ('ua-spoof', pageStore.pageHostname) ) { @@ -267,83 +275,63 @@ var onHeadersReceived = function(details) { var onMainDocHeadersReceived = function(details) { var µm = µMatrix; + var tabId = details.tabId; + var requestURL = details.url; // https://github.com/gorhill/uMatrix/issues/145 // Check if the main_frame is a download - if ( headerValue(details.responseHeaders, 'content-disposition').lastIndexOf('attachment', 0) === 0 ) { - µm.tabContextManager.unpush(details.tabId, details.url); + if ( headerValue(details.responseHeaders, 'content-type').lastIndexOf('application/x-', 0) === 0 ) { + µm.tabContextManager.unpush(tabId, requestURL); + } else { + µm.tabContextManager.push(tabId, requestURL); } - var tabContext = µm.tabContextManager.lookup(details.tabId); + + var tabContext = µm.tabContextManager.lookup(tabId); if ( tabContext === null ) { return; } - // console.debug('onMainDocHeadersReceived()> "%s": %o', details.url, details); + // console.debug('onMainDocHeadersReceived()> "%s": %o', requestURL, details); - // rhill 2013-12-07: - // Apparently in Opera, onBeforeRequest() is triggered while the - // URL is not yet bound to a tab (-1), which caused the code here - // to not be able to lookup the page store. So let the code here bind - // the page to a tab if not done yet. - // https://github.com/gorhill/httpswitchboard/issues/75 - - // TODO: check this works fine on Opera - - // Re-classify orphan HTTP requests as behind-the-scene requests. There is - // not much else which can be done, because there are URLs - // which cannot be handled by HTTP Switchboard, i.e. `opera://startpage`, - // as this would lead to complications with no obvious solution, like how - // to scope on unknown scheme? Etc. - // https://github.com/gorhill/httpswitchboard/issues/191 - // https://github.com/gorhill/httpswitchboard/issues/91#issuecomment-37180275 - var pageStore = µm.mustPageStoreFromTabId(details.tabId); - var headers = details.responseHeaders; - - // Maybe modify inbound headers - var csp = ''; - - // Enforce strict HTTPS? - var requestScheme = µm.URI.schemeFromURI(details.url); - if ( requestScheme === 'https' && µm.tMatrix.evaluateSwitchZ('https-strict', tabContext.rootHostname) ) { - csp += "default-src chrome-search: data: https: wss: 'unsafe-eval' 'unsafe-inline';"; - } + var blockScript = µm.mustBlock(tabContext.rootHostname, tabContext.rootHostname, 'script'); // https://github.com/gorhill/httpswitchboard/issues/181 - pageStore.pageScriptBlocked = µm.mustBlock(tabContext.rootHostname, tabContext.rootHostname, 'script'); - if ( pageStore.pageScriptBlocked ) { - // If javascript not allowed, say so through a `Content-Security-Policy` directive. - // console.debug('onMainDocHeadersReceived()> PAGE CSP "%s": %o', details.url, details); - csp += " script-src 'none'"; + var pageStore = µm.pageStoreFromTabId(tabId); + if ( pageStore ) { + pageStore.pageScriptBlocked = blockScript; } - // https://github.com/gorhill/httpswitchboard/issues/181 - if ( csp !== '' ) { - headers.push({ - 'name': 'Content-Security-Policy', - 'value': csp.trim() - }); - return { responseHeaders: headers }; + if ( !blockScript ) { + return; } + + µm.logger.writeOne(tabId, 'net', '---', 'inline-script', requestURL); + + // If javascript not allowed, say so through a `Content-Security-Policy` directive. + details.responseHeaders.push({ + 'name': 'Content-Security-Policy', + 'value': "script-src 'none'" + }); + return { responseHeaders: details.responseHeaders }; }; /******************************************************************************/ var onSubDocHeadersReceived = function(details) { + var µm = µMatrix; + var tabId = details.tabId; // console.debug('onSubDocHeadersReceived()> "%s": %o', details.url, details); - var µm = µMatrix; - // Do not ignore traffic outside tabs. // https://github.com/gorhill/httpswitchboard/issues/91#issuecomment-37180275 - var tabId = details.tabId; var tabContext = µm.tabContextManager.lookup(tabId); if ( tabContext === null ) { return; } // Evaluate - if ( µm.mustAllow(tabContext.rootHostname, µm.hostnameFromURL(details.url), 'script') ) { + if ( µm.mustAllow(tabContext.rootHostname, details.hostname, 'script') ) { return; } @@ -368,6 +356,9 @@ var onSubDocHeadersReceived = function(details) { // console.debug('onSubDocHeadersReceived()> FRAME CSP "%s": %o, scope="%s"', details.url, details, pageURL); + µm.logger.writeOne(tabId, 'net', '---', 'inline-script', details.url); + + // If javascript not allowed, say so through a `Content-Security-Policy` directive. details.responseHeaders.push({ 'name': 'Content-Security-Policy', 'value': "script-src 'none'" diff --git a/src/js/uritools.js b/src/js/uritools.js index 2b48815..8f4d082 100644 --- a/src/js/uritools.js +++ b/src/js/uritools.js @@ -236,6 +236,14 @@ URI.schemeFromURI = function(uri) { /******************************************************************************/ +URI.isSecureScheme = function(scheme) { + return scheme === 'https' || + scheme === 'wss' || + scheme === 'ftps'; +}; + +/******************************************************************************/ + URI.authorityFromURI = function(uri) { var matches = reAuthorityFromURI.exec(uri); if ( !matches ) {