diff --git a/platform/chromium/vapi-background.js b/platform/chromium/vapi-background.js index 8778a07e3..5e95622d6 100644 --- a/platform/chromium/vapi-background.js +++ b/platform/chromium/vapi-background.js @@ -858,7 +858,8 @@ vAPI.net.registerListeners = function() { // https://bugs.chromium.org/p/chromium/issues/detail?id=410382 // Between Chromium 38-48, plug-ins' network requests were reported as // type "other" instead of "object". - var is_v38_48 = /\bChrom[a-z]+\/(?:3[89]|4[0-8])\.[\d.]+\b/.test(navigator.userAgent); + var is_v38_48 = /\bChrom[a-z]+\/(?:3[89]|4[0-8])\.[\d.]+\b/.test(navigator.userAgent), + is_v49_55 = /\bChrom[a-z]+\/(?:49|5[012345])\b/.test(navigator.userAgent); // Chromium-based browsers understand only these network request types. var validTypes = { @@ -1003,9 +1004,20 @@ vAPI.net.registerListeners = function() { return onBeforeRequestClient(details); }; - var onHeadersReceivedClient = this.onHeadersReceived.callback; - var onHeadersReceivedClientTypes = this.onHeadersReceived.types.slice(0); - var onHeadersReceivedTypes = denormalizeTypes(onHeadersReceivedClientTypes); + // This is needed for Chromium 49-55. + var onBeforeSendHeaders = function(details) { + if ( details.type !== 'ping' || details.method !== 'POST' ) { return; } + var type = headerValue(details.requestHeaders, 'content-type'); + if ( type === '' ) { return; } + if ( type.endsWith('/csp-report') ) { + details.type = 'csp_report'; + return onBeforeRequestClient(details); + } + }; + + var onHeadersReceivedClient = this.onHeadersReceived.callback, + onHeadersReceivedClientTypes = this.onHeadersReceived.types.slice(0), + onHeadersReceivedTypes = denormalizeTypes(onHeadersReceivedClientTypes); var onHeadersReceived = function(details) { normalizeRequestDetails(details); // Hack to work around Chromium API limitations, where requests of @@ -1033,19 +1045,17 @@ vAPI.net.registerListeners = function() { }; var installListeners = (function() { - var listener; var crapi = chrome.webRequest; - listener = onBeforeRequest; //listener = function(details) { // quickProfiler.start('onBeforeRequest'); // var r = onBeforeRequest(details); // quickProfiler.stop(); // return r; //}; - if ( crapi.onBeforeRequest.hasListener(listener) === false ) { + if ( crapi.onBeforeRequest.hasListener(onBeforeRequest) === false ) { crapi.onBeforeRequest.addListener( - listener, + onBeforeRequest, { 'urls': this.onBeforeRequest.urls || [''], 'types': this.onBeforeRequest.types || undefined @@ -1054,10 +1064,22 @@ vAPI.net.registerListeners = function() { ); } - listener = onHeadersReceived; - if ( crapi.onHeadersReceived.hasListener(listener) === false ) { + // Chromium 48 and lower does not support `ping` type. + // Chromium 56 and higher does support `csp_report` stype. + if ( is_v49_55 && crapi.onBeforeSendHeaders.hasListener(onBeforeSendHeaders) === false ) { + crapi.onBeforeSendHeaders.addListener( + onBeforeSendHeaders, + { + 'urls': [ '' ], + 'types': [ 'ping' ] + }, + [ 'blocking', 'requestHeaders' ] + ); + } + + if ( crapi.onHeadersReceived.hasListener(onHeadersReceived) === false ) { crapi.onHeadersReceived.addListener( - listener, + onHeadersReceived, { 'urls': this.onHeadersReceived.urls || [''], 'types': onHeadersReceivedTypes diff --git a/platform/firefox/vapi-background.js b/platform/firefox/vapi-background.js index 2c1dc9a26..188ab1a6d 100644 --- a/platform/firefox/vapi-background.js +++ b/platform/firefox/vapi-background.js @@ -260,15 +260,12 @@ vAPI.browserSettings = { case 'hyperlinkAuditing': this.rememberOriginalValue('browser', 'send_pings'); - this.rememberOriginalValue('beacon', 'enabled'); // https://github.com/gorhill/uBlock/issues/292 // "true" means "do not disable", i.e. leave entry alone if ( settingVal ) { this.clear('browser', 'send_pings'); - this.clear('beacon', 'enabled'); } else { this.setValue('browser', 'send_pings', false); - this.setValue('beacon', 'enabled', false); } break; @@ -1892,6 +1889,7 @@ var httpObserver = { 14: 'font', 15: 'media', 16: 'websocket', + 17: 'csp_report', 19: 'beacon', 21: 'image' }, @@ -2156,8 +2154,8 @@ var httpObserver = { // 'Content-Security-Policy' MUST come last in the array. Need to // revised this eventually. - var responseHeaders = []; - var value = channel.contentLength; + var responseHeaders = [], + value = channel.contentLength; if ( value !== -1 ) { responseHeaders.push({ name: 'Content-Length', value: value }); } @@ -2339,9 +2337,6 @@ vAPI.net = {}; /******************************************************************************/ vAPI.net.registerListeners = function() { - // Since it's not used - this.onBeforeSendHeaders = null; - if ( typeof this.onBeforeRequest.callback === 'function' ) { httpObserver.onBeforeRequest = this.onBeforeRequest.callback; httpObserver.onBeforeRequestTypes = this.onBeforeRequest.types ? diff --git a/src/_locales/en/messages.json b/src/_locales/en/messages.json index 890e4d427..561c0f08a 100644 --- a/src/_locales/en/messages.json +++ b/src/_locales/en/messages.json @@ -212,7 +212,7 @@ "description":"English: " }, "settingsHyperlinkAuditingDisabledPrompt":{ - "message":"Disable hyperlink auditing/beacon", + "message":"Disable hyperlink auditing", "description":"English: " }, "settingsWebRTCIPAddressHiddenPrompt":{ diff --git a/src/js/pagestore.js b/src/js/pagestore.js index e4860cfaa..36ec7e276 100644 --- a/src/js/pagestore.js +++ b/src/js/pagestore.js @@ -313,6 +313,7 @@ PageStore.prototype.init = function(tabId) { this.largeMediaCount = 0; this.largeMediaTimer = null; this.netFilteringCache = NetFilteringResultCache.factory(); + this.internalRedirectionCount = 0; this.noCosmeticFiltering = µb.hnSwitches.evaluateZ('no-cosmetic-filtering', tabContext.rootHostname) === true; if ( µb.logger.isEnabled() && this.noCosmeticFiltering ) { @@ -614,10 +615,15 @@ PageStore.prototype.journalProcess = function(fromTimer) { PageStore.prototype.filterRequest = function(context) { var requestType = context.requestType; + // We want to short-term cache filtering results of collapsible types, + // because they are likely to be reused, from network request handler and + // from content script handler. + if ( 'image sub_frame object'.indexOf(requestType) === -1 ) { + return this.filterRequestNoCache(context); + } + if ( this.getNetFilteringSwitch() === false ) { - if ( collapsibleRequestTypes.indexOf(requestType) !== -1 ) { - this.netFilteringCache.add(context, ''); - } + this.netFilteringCache.add(context, ''); return ''; } @@ -629,23 +635,13 @@ PageStore.prototype.filterRequest = function(context) { var result = ''; - if ( requestType === 'font' ) { - if ( µb.hnSwitches.evaluateZ('no-remote-fonts', context.rootHostname) !== false ) { - result = µb.hnSwitches.toResultString(); - } - this.remoteFontCount += 1; - } - + // Dynamic URL filtering. if ( result === '' ) { µb.sessionURLFiltering.evaluateZ(context.rootHostname, context.requestURL, requestType); result = µb.sessionURLFiltering.toFilterString(); } - // Given that: - // - Dynamic filtering override static filtering - // - Evaluating dynamic filtering is much faster than static filtering - // We evaluate dynamic filtering first, and hopefully we can skip - // evaluation of static filtering. + // Dynamic hostname/type filtering. if ( result === '' && µb.userSettings.advancedUserEnabled ) { µb.sessionFirewall.evaluateCellZY( context.rootHostname, context.requestHostname, requestType); if ( µb.sessionFirewall.mustBlockOrAllow() ) { @@ -653,7 +649,7 @@ PageStore.prototype.filterRequest = function(context) { } } - // Static filtering never override dynamic filtering + // Static filtering has lowest precedence. if ( result === '' || result.charAt(1) === 'n' ) { if ( µb.staticNetFilteringEngine.matchString(context) !== undefined ) { result = µb.staticNetFilteringEngine.toResultString(µb.logger.isEnabled()); @@ -661,18 +657,11 @@ PageStore.prototype.filterRequest = function(context) { } //console.debug('cache MISS: PageStore.filterRequest("%s")', context.requestURL); - if ( collapsibleRequestTypes.indexOf(requestType) !== -1 ) { - this.netFilteringCache.add(context, result); - } - - // console.debug('[%s, %s] = "%s"', context.requestHostname, requestType, result); + this.netFilteringCache.add(context, result); return result; }; -// http://jsperf.com/string-indexof-vs-object -var collapsibleRequestTypes = 'image sub_frame object'; - /******************************************************************************/ PageStore.prototype.filterRequestNoCache = function(context) { @@ -680,26 +669,27 @@ PageStore.prototype.filterRequestNoCache = function(context) { return ''; } - var requestType = context.requestType; - var result = ''; + var requestType = context.requestType, + result = ''; - if ( requestType === 'font' ) { + if ( requestType === 'csp_report' ) { + if ( this.internalRedirectionCount !== 0 ) { + result = 'gb:no-spurious-csp-report'; + } + } else if ( requestType === 'font' ) { if ( µb.hnSwitches.evaluateZ('no-remote-fonts', context.rootHostname) !== false ) { result = µb.hnSwitches.toResultString(); } this.remoteFontCount += 1; } + // Dynamic URL filtering. if ( result === '' ) { µb.sessionURLFiltering.evaluateZ(context.rootHostname, context.requestURL, requestType); result = µb.sessionURLFiltering.toFilterString(); } - // Given that: - // - Dynamic filtering override static filtering - // - Evaluating dynamic filtering is much faster than static filtering - // We evaluate dynamic filtering first, and hopefully we can skip - // evaluation of static filtering. + // Dynamic hostname/type filtering. if ( result === '' && µb.userSettings.advancedUserEnabled ) { µb.sessionFirewall.evaluateCellZY(context.rootHostname, context.requestHostname, requestType); if ( µb.sessionFirewall.mustBlockOrAllow() ) { @@ -707,7 +697,7 @@ PageStore.prototype.filterRequestNoCache = function(context) { } } - // Static filtering never override dynamic filtering + // Static filtering has lowest precedence. if ( result === '' || result.charAt(1) === 'n' ) { if ( µb.staticNetFilteringEngine.matchString(context) !== undefined ) { result = µb.staticNetFilteringEngine.toResultString(µb.logger.isEnabled()); diff --git a/src/js/traffic.js b/src/js/traffic.js index 38deba25c..07ec05939 100644 --- a/src/js/traffic.js +++ b/src/js/traffic.js @@ -45,12 +45,6 @@ var onBeforeRequest = function(details) { return onBeforeRootFrameRequest(details); } - // https://github.com/gorhill/uBlock/issues/870 - // This work for Chromium 49+. - if ( requestType === 'beacon' ) { - return onBeforeBeacon(details); - } - // Special treatment: behind-the-scene requests var tabId = details.tabId; if ( vAPI.isBehindTheSceneTabId(tabId) ) { @@ -58,8 +52,8 @@ var onBeforeRequest = function(details) { } // Lookup the page store associated with this tab id. - var µb = µBlock; - var pageStore = µb.pageStoreFromTabId(tabId); + var µb = µBlock, + pageStore = µb.pageStoreFromTabId(tabId); if ( !pageStore ) { var tabContext = µb.tabContextManager.mustLookup(tabId); if ( vAPI.isBehindTheSceneTabId(tabContext.tabId) ) { @@ -119,6 +113,7 @@ var onBeforeRequest = function(details) { // Redirect blocked request? var url = µb.redirectEngine.toURL(requestContext); if ( url !== undefined ) { + pageStore.internalRedirectionCount += 1; if ( µb.logger.isEnabled() ) { µb.logger.writeOne( tabId, @@ -277,62 +272,37 @@ var toBlockDocResult = function(url, hostname, result) { /******************************************************************************/ +// Intercept and filter behind-the-scene requests. + // https://github.com/gorhill/uBlock/issues/870 // Finally, Chromium 49+ gained the ability to report network request of type // `beacon`, so now we can block them according to the state of the // "Disable hyperlink auditing/beacon" setting. -var onBeforeBeacon = function(details) { - var µb = µBlock; - var tabId = details.tabId; - var pageStore = µb.mustPageStoreFromTabId(tabId); - var context = pageStore.createContextFromPage(); - context.requestURL = details.url; - context.requestHostname = µb.URI.hostnameFromURI(details.url); - context.requestType = details.type; - // "g" in "gb:" stands for "global setting" - var result = µb.userSettings.hyperlinkAuditingDisabled ? 'gb:' : ''; - pageStore.journalAddRequest(context.requestHostname, result); - if ( µb.logger.isEnabled() ) { - µb.logger.writeOne( - tabId, - 'net', - result, - details.type, - details.url, - context.rootHostname, - context.rootHostname - ); - } - context.dispose(); - if ( result !== '' ) { - return { cancel: true }; - } -}; - -/******************************************************************************/ - -// Intercept and filter behind-the-scene requests. - var onBeforeBehindTheSceneRequest = function(details) { - var µb = µBlock; - var pageStore = µb.pageStoreFromTabId(vAPI.noTabId); - if ( !pageStore ) { - return; - } + var µb = µBlock, + pageStore = µb.pageStoreFromTabId(vAPI.noTabId); + if ( !pageStore ) { return; } + + var result = '', + context = pageStore.createContextFromPage(), + requestType = details.type, + requestURL = details.url; - var context = pageStore.createContextFromPage(); - var requestURL = details.url; context.requestURL = requestURL; context.requestHostname = µb.URI.hostnameFromURI(requestURL); - context.requestType = details.type; + context.requestType = requestType; + + // "g" in "gb:" stands for "global setting" + if ( requestType === 'beacon' && µb.userSettings.hyperlinkAuditingDisabled ) { + result = 'gb:no-hyperlink-auditing'; + } // Blocking behind-the-scene requests can break a lot of stuff: prevent // browser updates, prevent extension updates, prevent extensions from // working properly, etc. // So we filter if and only if the "advanced user" mode is selected - var result = ''; - if ( µb.userSettings.advancedUserEnabled ) { + if ( result === '' && µb.userSettings.advancedUserEnabled ) { result = pageStore.filterRequestNoCache(context); } @@ -343,7 +313,7 @@ var onBeforeBehindTheSceneRequest = function(details) { vAPI.noTabId, 'net', result, - details.type, + requestType, requestURL, context.rootHostname, context.rootHostname