diff --git a/platform/edge/vapi-webrequest.js b/platform/edge/vapi-webrequest.js new file mode 100644 index 0000000000000..85a2842818bc1 --- /dev/null +++ b/platform/edge/vapi-webrequest.js @@ -0,0 +1,339 @@ +/******************************************************************************* + + uBlock Origin - a browser extension to block requests. + Copyright (C) 2017 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/uBlock +*/ + +// For background page + +'use strict'; + +/******************************************************************************/ + +vAPI.net = { + onBeforeRequest: {}, + onBeforeMaybeSpuriousCSPReport: {}, + onHeadersReceived: {}, + nativeCSPReportFiltering: false +}; + +vAPI.net.registerListeners = function() { + + var µb = µBlock, + µburi = µb.URI, + wrApi = chrome.webRequest; + + // 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); + + // legacy Chromium understands only these network request types. + var validTypes = { + main_frame: true, + sub_frame: true, + stylesheet: true, + script: true, + image: true, + object: true, + xmlhttprequest: true, + other: true + }; + // modern Chromium/WebExtensions: more types available. + if ( wrApi.ResourceType ) { + (function() { + for ( var typeKey in wrApi.ResourceType ) { + if ( wrApi.ResourceType.hasOwnProperty(typeKey) ) { + validTypes[wrApi.ResourceType[typeKey]] = true; + } + } + })(); + } + + var extToTypeMap = new Map([ + ['eot','font'],['otf','font'],['svg','font'],['ttf','font'],['woff','font'],['woff2','font'], + ['mp3','media'],['mp4','media'],['webm','media'], + ['gif','image'],['ico','image'],['jpeg','image'],['jpg','image'],['png','image'],['webp','image'] + ]); + + var denormalizeTypes = function(aa) { + if ( aa.length === 0 ) { + return Object.keys(validTypes); + } + var out = []; + var i = aa.length, + type, + needOther = true; + while ( i-- ) { + type = aa[i]; + if ( validTypes[type] ) { + out.push(type); + } + if ( type === 'other' ) { + needOther = false; + } + } + if ( needOther ) { + out.push('other'); + } + return out; + }; + + var headerValue = function(headers, name) { + var i = headers.length; + while ( i-- ) { + if ( headers[i].name.toLowerCase() === name ) { + return headers[i].value.trim(); + } + } + return ''; + }; + + var normalizeRequestDetails = function(details) { + details.tabId = details.tabId.toString(); + + var type = details.type; + + // https://github.com/gorhill/uBlock/issues/1493 + // Chromium 49+/WebExtensions support a new request type: `ping`, + // which is fired as a result of using `navigator.sendBeacon`. + if ( type === 'ping' ) { + details.type = 'beacon'; + return; + } + + if ( type === 'imageset' ) { + details.type = 'image'; + return; + } + + // The rest of the function code is to normalize type + if ( type !== 'other' ) { + return; + } + + // Try to map known "extension" part of URL to request type. + var path = µburi.pathFromURI(details.url), + pos = path.indexOf('.', path.length - 6); + if ( pos !== -1 && (type = extToTypeMap.get(path.slice(pos + 1))) ) { + details.type = type; + return; + } + + // Try to extract type from response headers if present. + if ( details.responseHeaders ) { + type = headerValue(details.responseHeaders, 'content-type'); + if ( type.startsWith('font/') ) { + details.type = 'font'; + return; + } + if ( type.startsWith('image/') ) { + details.type = 'image'; + return; + } + if ( type.startsWith('audio/') || type.startsWith('video/') ) { + details.type = 'media'; + return; + } + } + + // https://github.com/chrisaljoudi/uBlock/issues/862 + // If no transposition possible, transpose to `object` as per + // Chromium bug 410382 + // https://code.google.com/p/chromium/issues/detail?id=410382 + if ( is_v38_48 ) { + details.type = 'object'; + } + }; + + // https://bugs.chromium.org/p/chromium/issues/detail?id=129353 + // https://github.com/gorhill/uBlock/issues/1497 + // Expose websocket-based network requests to uBO's filtering engine, + // logger, etc. + // Counterpart of following block of code is found in "vapi-client.js" -- + // search for "https://github.com/gorhill/uBlock/issues/1497". + // + // Once uBO 1.11.1 and uBO-Extra 2.12 are widespread, the image-based + // handling code can be removed. + var onBeforeWebsocketRequest = function(details) { + if ( (details.type !== 'image') && + (details.method !== 'HEAD' || details.type !== 'xmlhttprequest') + ) { + return; + } + var requestURL = details.url, + matches = /[?&]u(?:rl)?=([^&]+)/.exec(requestURL); + if ( matches === null ) { return; } + details.type = 'websocket'; + details.url = decodeURIComponent(matches[1]); + var r = onBeforeRequestClient(details); + if ( r && r.cancel ) { return r; } + // Redirect to the provided URL, or a 1x1 data: URI if none provided. + matches = /[?&]r=([^&]+)/.exec(requestURL); + return { + redirectUrl: matches !== null ? + decodeURIComponent(matches[1]) : + 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==' + }; + }; + + var onBeforeRequestClient = this.onBeforeRequest.callback; + var onBeforeRequest = validTypes.websocket + // modern Chromium/WebExtensions: type 'websocket' is supported + ? function(details) { + normalizeRequestDetails(details); + return onBeforeRequestClient(details); + } + // legacy Chromium + : function(details) { + // https://github.com/gorhill/uBlock/issues/1497 + if ( details.url.endsWith('ubofix=f41665f3028c7fd10eecf573336216d3') ) { + var r = onBeforeWebsocketRequest(details); + if ( r !== undefined ) { return r; } + } + normalizeRequestDetails(details); + return onBeforeRequestClient(details); + }; + + // This is needed for Chromium 49-55. + var onBeforeSendHeaders = validTypes.csp_report + // modern Chromium/WebExtensions: type 'csp_report' is supported + ? null + // legacy Chromium + : 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 = validTypes.font + // modern Chromium/WebExtensions: type 'font' is supported + ? function(details) { + normalizeRequestDetails(details); + if ( + onHeadersReceivedClientTypes.length !== 0 && + onHeadersReceivedClientTypes.indexOf(details.type) === -1 + ) { + return; + } + return onHeadersReceivedClient(details); + } + // legacy Chromium + : function(details) { + normalizeRequestDetails(details); + // Hack to work around Chromium API limitations, where requests of + // type `font` are returned as `other`. For example, our normalization + // fail at transposing `other` into `font` for URLs which are outside + // what is expected. At least when headers are received we can check + // for content type `font/*`. Blocking at onHeadersReceived time is + // less worse than not blocking at all. Also, due to Chromium bug, + // `other` always becomes `object` when it can't be normalized into + // something else. Test case for "unfriendly" font URLs: + // https://www.google.com/fonts + if ( details.type === 'font' ) { + var r = onBeforeRequestClient(details); + if ( typeof r === 'object' && r.cancel === true ) { + return { cancel: true }; + } + } + if ( + onHeadersReceivedClientTypes.length !== 0 && + onHeadersReceivedClientTypes.indexOf(details.type) === -1 + ) { + return; + } + return onHeadersReceivedClient(details); + }; + + var urls, types; + + if ( onBeforeRequest ) { + urls = this.onBeforeRequest.urls || ['']; + types = this.onBeforeRequest.types || undefined; + if ( + (validTypes.websocket) && + (types === undefined || types.indexOf('websocket') !== -1) && + (urls.indexOf('') === -1) + ) { + if ( urls.indexOf('ws://*/*') === -1 ) { + urls.push('ws://*/*'); + } + if ( urls.indexOf('wss://*/*') === -1 ) { + urls.push('wss://*/*'); + } + } + wrApi.onBeforeRequest.addListener( + onBeforeRequest, + { urls: urls, types: types }, + this.onBeforeRequest.extra + ); + } + + // https://github.com/gorhill/uBlock/issues/3140 + this.nativeCSPReportFiltering = validTypes.csp_report; + if ( + this.nativeCSPReportFiltering && + typeof this.onBeforeMaybeSpuriousCSPReport.callback === 'function' + ) { + wrApi.onBeforeRequest.addListener( + this.onBeforeMaybeSpuriousCSPReport.callback, + { + urls: [ 'http://*/*', 'https://*/*' ], + types: [ 'csp_report' ] + }, + [ 'blocking', 'requestBody' ] + ); + } + + // Chromium 48 and lower does not support `ping` type. + // Chromium 56 and higher does support `csp_report` stype. + if ( onBeforeSendHeaders ) { + // Patch 2018-01-05: This throws in Edge + try { + wrApi.onBeforeSendHeaders.addListener( + onBeforeSendHeaders, + { + 'urls': [ '' ], + 'types': [ 'ping' ] + }, + [ 'blocking', 'requestHeaders' ] + ); + } catch ( err ) { + console.log(err); + } + } + + if ( onHeadersReceived ) { + urls = this.onHeadersReceived.urls || ['']; + types = onHeadersReceivedTypes; + wrApi.onHeadersReceived.addListener( + onHeadersReceived, + { urls: urls, types: types }, + this.onHeadersReceived.extra + ); + } +}; + +/******************************************************************************/