mirror of
https://github.com/gorhill/uBlock.git
synced 2026-03-11 09:04:36 +00:00
Improve prevent-xhr scriptlet
This commit is contained in:
parent
84434e0e7e
commit
168394440c
2 changed files with 271 additions and 226 deletions
270
src/js/resources/prevent-xhr.js
Normal file
270
src/js/resources/prevent-xhr.js
Normal file
|
|
@ -0,0 +1,270 @@
|
|||
/*******************************************************************************
|
||||
|
||||
uBlock Origin - a comprehensive, efficient content blocker
|
||||
Copyright (C) 2019-present 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
|
||||
|
||||
*/
|
||||
|
||||
import {
|
||||
generateContentFn,
|
||||
matchObjectPropertiesFn,
|
||||
parsePropertiesToMatchFn,
|
||||
} from './utils.js';
|
||||
import { proxyApplyFn } from './proxy-apply.js';
|
||||
import { registerScriptlet } from './base.js';
|
||||
import { safeSelf } from './safe-self.js';
|
||||
|
||||
// Externally added to the private namespace in which scriptlets execute.
|
||||
/* global scriptletGlobals */
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
function preventXhrFn(
|
||||
trusted = false,
|
||||
propsToMatch = '',
|
||||
directive = ''
|
||||
) {
|
||||
if ( typeof propsToMatch !== 'string' ) { return; }
|
||||
const safe = safeSelf();
|
||||
const scriptletName = trusted ? 'trusted-prevent-xhr' : 'prevent-xhr';
|
||||
const logPrefix = safe.makeLogPrefix(scriptletName, propsToMatch, directive);
|
||||
const xhrInstances = new WeakMap();
|
||||
const propNeedles = parsePropertiesToMatchFn(propsToMatch, 'url');
|
||||
const warOrigin = scriptletGlobals.warOrigin;
|
||||
const safeDispatchEvent = (xhr, type) => {
|
||||
try {
|
||||
xhr.dispatchEvent(new Event(type));
|
||||
} catch {
|
||||
}
|
||||
};
|
||||
proxyApplyFn('XMLHttpRequest.prototype.open', function(context) {
|
||||
const { thisArg, callArgs } = context;
|
||||
xhrInstances.delete(thisArg);
|
||||
const [ method, url, ...args ] = callArgs;
|
||||
if ( warOrigin !== undefined && url.startsWith(warOrigin) ) {
|
||||
return context.reflect();
|
||||
}
|
||||
const haystack = { method, url };
|
||||
if ( propsToMatch === '' && directive === '' ) {
|
||||
safe.uboLog(logPrefix, `Called: ${safe.JSON_stringify(haystack, null, 2)}`);
|
||||
return context.reflect();
|
||||
}
|
||||
if ( matchObjectPropertiesFn(propNeedles, haystack) ) {
|
||||
const xhrDetails = Object.assign(haystack, {
|
||||
xhr: thisArg,
|
||||
defer: args.length === 0 || !!args[0],
|
||||
directive,
|
||||
headers: {
|
||||
'date': '',
|
||||
'content-type': '',
|
||||
'content-length': '',
|
||||
},
|
||||
url: haystack.url,
|
||||
props: {
|
||||
response: { value: '' },
|
||||
responseText: { value: '' },
|
||||
responseXML: { value: null },
|
||||
},
|
||||
});
|
||||
xhrInstances.set(thisArg, xhrDetails);
|
||||
}
|
||||
return context.reflect();
|
||||
});
|
||||
proxyApplyFn('XMLHttpRequest.prototype.send', function(context) {
|
||||
const { thisArg } = context;
|
||||
const xhrDetails = xhrInstances.get(thisArg);
|
||||
if ( xhrDetails === undefined ) {
|
||||
return context.reflect();
|
||||
}
|
||||
xhrDetails.headers['date'] = (new Date()).toUTCString();
|
||||
let xhrText = '';
|
||||
switch ( thisArg.responseType ) {
|
||||
case 'arraybuffer':
|
||||
xhrDetails.props.response.value = new ArrayBuffer(0);
|
||||
xhrDetails.headers['content-type'] = 'application/octet-stream';
|
||||
break;
|
||||
case 'blob':
|
||||
xhrDetails.props.response.value = new Blob([]);
|
||||
xhrDetails.headers['content-type'] = 'application/octet-stream';
|
||||
break;
|
||||
case 'document': {
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString('', 'text/html');
|
||||
xhrDetails.props.response.value = doc;
|
||||
xhrDetails.props.responseXML.value = doc;
|
||||
xhrDetails.headers['content-type'] = 'text/html';
|
||||
break;
|
||||
}
|
||||
case 'json':
|
||||
xhrDetails.props.response.value = {};
|
||||
xhrDetails.props.responseText.value = '{}';
|
||||
xhrDetails.headers['content-type'] = 'application/json';
|
||||
break;
|
||||
default: {
|
||||
if ( directive === '' ) { break; }
|
||||
xhrText = generateContentFn(trusted, xhrDetails.directive);
|
||||
if ( xhrText instanceof Promise ) {
|
||||
xhrText = xhrText.then(text => {
|
||||
xhrDetails.props.response.value = text;
|
||||
xhrDetails.props.responseText.value = text;
|
||||
});
|
||||
} else {
|
||||
xhrDetails.props.response.value = xhrText;
|
||||
xhrDetails.props.responseText.value = xhrText;
|
||||
}
|
||||
xhrDetails.headers['content-type'] = 'text/plain';
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ( xhrDetails.defer === false ) {
|
||||
xhrDetails.headers['content-length'] = `${xhrDetails.props.response.value}`.length;
|
||||
Object.defineProperties(xhrDetails.xhr, {
|
||||
readyState: { value: 4 },
|
||||
responseURL: { value: xhrDetails.url },
|
||||
status: { value: 200 },
|
||||
statusText: { value: 'OK' },
|
||||
});
|
||||
Object.defineProperties(xhrDetails.xhr, xhrDetails.props);
|
||||
return;
|
||||
}
|
||||
Promise.resolve(xhrText).then(( ) => xhrDetails).then(details => {
|
||||
Object.defineProperties(details.xhr, {
|
||||
readyState: { value: 1, configurable: true },
|
||||
responseURL: { value: xhrDetails.url },
|
||||
});
|
||||
safeDispatchEvent(details.xhr, 'readystatechange');
|
||||
return details;
|
||||
}).then(details => {
|
||||
xhrDetails.headers['content-length'] = `${details.props.response.value}`.length;
|
||||
Object.defineProperties(details.xhr, {
|
||||
readyState: { value: 2, configurable: true },
|
||||
status: { value: 200 },
|
||||
statusText: { value: 'OK' },
|
||||
});
|
||||
safeDispatchEvent(details.xhr, 'readystatechange');
|
||||
return details;
|
||||
}).then(details => {
|
||||
Object.defineProperties(details.xhr, {
|
||||
readyState: { value: 3, configurable: true },
|
||||
});
|
||||
Object.defineProperties(details.xhr, details.props);
|
||||
safeDispatchEvent(details.xhr, 'readystatechange');
|
||||
return details;
|
||||
}).then(details => {
|
||||
Object.defineProperties(details.xhr, {
|
||||
readyState: { value: 4 },
|
||||
});
|
||||
safeDispatchEvent(details.xhr, 'readystatechange');
|
||||
safeDispatchEvent(details.xhr, 'load');
|
||||
safeDispatchEvent(details.xhr, 'loadend');
|
||||
safe.uboLog(logPrefix, `Prevented with response:\n${details.xhr.response}`);
|
||||
});
|
||||
});
|
||||
proxyApplyFn('XMLHttpRequest.prototype.getResponseHeader', function(context) {
|
||||
const { thisArg } = context;
|
||||
const xhrDetails = xhrInstances.get(thisArg);
|
||||
if ( xhrDetails === undefined || thisArg.readyState < thisArg.HEADERS_RECEIVED ) {
|
||||
return context.reflect();
|
||||
}
|
||||
const headerName = `${context.callArgs[0]}`;
|
||||
const value = xhrDetails.headers[headerName.toLowerCase()];
|
||||
if ( value !== undefined && value !== '' ) { return value; }
|
||||
return null;
|
||||
});
|
||||
proxyApplyFn('XMLHttpRequest.prototype.getAllResponseHeaders', function(context) {
|
||||
const { thisArg } = context;
|
||||
const xhrDetails = xhrInstances.get(thisArg);
|
||||
if ( xhrDetails === undefined || thisArg.readyState < thisArg.HEADERS_RECEIVED ) {
|
||||
return context.reflect();
|
||||
}
|
||||
const out = [];
|
||||
for ( const [ name, value ] of Object.entries(xhrDetails.headers) ) {
|
||||
if ( !value ) { continue; }
|
||||
out.push(`${name}: ${value}`);
|
||||
}
|
||||
if ( out.length !== 0 ) { out.push(''); }
|
||||
return out.join('\r\n');
|
||||
});
|
||||
}
|
||||
registerScriptlet(preventXhrFn, {
|
||||
name: 'prevent-xhr.fn',
|
||||
dependencies: [
|
||||
generateContentFn,
|
||||
matchObjectPropertiesFn,
|
||||
parsePropertiesToMatchFn,
|
||||
proxyApplyFn,
|
||||
safeSelf,
|
||||
],
|
||||
});
|
||||
|
||||
/******************************************************************************/
|
||||
/**
|
||||
* @scriptlet prevent-xhr
|
||||
*
|
||||
* @description
|
||||
* Prevent a XMLHttpRequest-baesed request from being sent to a remote server.
|
||||
*
|
||||
* @param propsToMatch
|
||||
* The fetch arguments to match for the prevention to be triggered. The
|
||||
* untrusted flavor limits the realm of response to return to safe values.
|
||||
*
|
||||
* @param [responseBody]
|
||||
* Optional. The reponse to return when the prevention occurs. The response
|
||||
* must be a safe constant value.
|
||||
*
|
||||
* */
|
||||
|
||||
function preventXhr(...args) {
|
||||
return preventXhrFn(false, ...args);
|
||||
}
|
||||
registerScriptlet(preventXhr, {
|
||||
name: 'prevent-xhr.js',
|
||||
aliases: [
|
||||
'no-xhr-if.js',
|
||||
],
|
||||
dependencies: [
|
||||
preventXhrFn,
|
||||
],
|
||||
});
|
||||
|
||||
/******************************************************************************/
|
||||
/**
|
||||
* @scriptlet trusted-prevent-xhr
|
||||
*
|
||||
* @description
|
||||
* Prevent a XMLHttpRequest-based request from being sent to a remote server.
|
||||
*
|
||||
* @param propsToMatch
|
||||
* The fetch arguments to match for the prevention to be triggered. The
|
||||
* untrusted flavor limits the realm of response to return to safe values.
|
||||
*
|
||||
* @param [responseBody]
|
||||
* Optional. The reponse to return when the prevention occurs. The trusted
|
||||
* version allows arbitrary response.
|
||||
*
|
||||
* */
|
||||
|
||||
function trustedPreventXhr(...args) {
|
||||
return preventXhrFn(true, ...args);
|
||||
}
|
||||
registerScriptlet(trustedPreventXhr, {
|
||||
name: 'trusted-prevent-xhr.js',
|
||||
dependencies: [
|
||||
preventXhrFn,
|
||||
],
|
||||
});
|
||||
|
|
@ -32,12 +32,12 @@ import './prevent-dialog.js';
|
|||
import './prevent-fetch.js';
|
||||
import './prevent-innerHTML.js';
|
||||
import './prevent-settimeout.js';
|
||||
import './prevent-xhr.js';
|
||||
import './replace-argument.js';
|
||||
import './spoof-css.js';
|
||||
|
||||
import {
|
||||
collateFetchArgumentsFn,
|
||||
generateContentFn,
|
||||
getExceptionTokenFn,
|
||||
getRandomTokenFn,
|
||||
matchObjectPropertiesFn,
|
||||
|
|
@ -381,196 +381,6 @@ function replaceFetchResponseFn(
|
|||
});
|
||||
}
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
builtinScriptlets.push({
|
||||
name: 'prevent-xhr.fn',
|
||||
fn: preventXhrFn,
|
||||
dependencies: [
|
||||
'generate-content.fn',
|
||||
'match-object-properties.fn',
|
||||
'parse-properties-to-match.fn',
|
||||
'safe-self.fn',
|
||||
],
|
||||
});
|
||||
function preventXhrFn(
|
||||
trusted = false,
|
||||
propsToMatch = '',
|
||||
directive = ''
|
||||
) {
|
||||
if ( typeof propsToMatch !== 'string' ) { return; }
|
||||
const safe = safeSelf();
|
||||
const scriptletName = trusted ? 'trusted-prevent-xhr' : 'prevent-xhr';
|
||||
const logPrefix = safe.makeLogPrefix(scriptletName, propsToMatch, directive);
|
||||
const xhrInstances = new WeakMap();
|
||||
const propNeedles = parsePropertiesToMatchFn(propsToMatch, 'url');
|
||||
const warOrigin = scriptletGlobals.warOrigin;
|
||||
const safeDispatchEvent = (xhr, type) => {
|
||||
try {
|
||||
xhr.dispatchEvent(new Event(type));
|
||||
} catch {
|
||||
}
|
||||
};
|
||||
const XHRBefore = XMLHttpRequest.prototype;
|
||||
self.XMLHttpRequest = class extends self.XMLHttpRequest {
|
||||
open(method, url, ...args) {
|
||||
xhrInstances.delete(this);
|
||||
if ( warOrigin !== undefined && url.startsWith(warOrigin) ) {
|
||||
return super.open(method, url, ...args);
|
||||
}
|
||||
const haystack = { method, url };
|
||||
if ( propsToMatch === '' && directive === '' ) {
|
||||
safe.uboLog(logPrefix, `Called: ${safe.JSON_stringify(haystack, null, 2)}`);
|
||||
return super.open(method, url, ...args);
|
||||
}
|
||||
if ( matchObjectPropertiesFn(propNeedles, haystack) ) {
|
||||
const xhrDetails = Object.assign(haystack, {
|
||||
xhr: this,
|
||||
defer: args.length === 0 || !!args[0],
|
||||
directive,
|
||||
headers: {
|
||||
'date': '',
|
||||
'content-type': '',
|
||||
'content-length': '',
|
||||
},
|
||||
url: haystack.url,
|
||||
props: {
|
||||
response: { value: '' },
|
||||
responseText: { value: '' },
|
||||
responseXML: { value: null },
|
||||
},
|
||||
});
|
||||
xhrInstances.set(this, xhrDetails);
|
||||
}
|
||||
return super.open(method, url, ...args);
|
||||
}
|
||||
send(...args) {
|
||||
const xhrDetails = xhrInstances.get(this);
|
||||
if ( xhrDetails === undefined ) {
|
||||
return super.send(...args);
|
||||
}
|
||||
xhrDetails.headers['date'] = (new Date()).toUTCString();
|
||||
let xhrText = '';
|
||||
switch ( this.responseType ) {
|
||||
case 'arraybuffer':
|
||||
xhrDetails.props.response.value = new ArrayBuffer(0);
|
||||
xhrDetails.headers['content-type'] = 'application/octet-stream';
|
||||
break;
|
||||
case 'blob':
|
||||
xhrDetails.props.response.value = new Blob([]);
|
||||
xhrDetails.headers['content-type'] = 'application/octet-stream';
|
||||
break;
|
||||
case 'document': {
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString('', 'text/html');
|
||||
xhrDetails.props.response.value = doc;
|
||||
xhrDetails.props.responseXML.value = doc;
|
||||
xhrDetails.headers['content-type'] = 'text/html';
|
||||
break;
|
||||
}
|
||||
case 'json':
|
||||
xhrDetails.props.response.value = {};
|
||||
xhrDetails.props.responseText.value = '{}';
|
||||
xhrDetails.headers['content-type'] = 'application/json';
|
||||
break;
|
||||
default: {
|
||||
if ( directive === '' ) { break; }
|
||||
xhrText = generateContentFn(trusted, xhrDetails.directive);
|
||||
if ( xhrText instanceof Promise ) {
|
||||
xhrText = xhrText.then(text => {
|
||||
xhrDetails.props.response.value = text;
|
||||
xhrDetails.props.responseText.value = text;
|
||||
});
|
||||
} else {
|
||||
xhrDetails.props.response.value = xhrText;
|
||||
xhrDetails.props.responseText.value = xhrText;
|
||||
}
|
||||
xhrDetails.headers['content-type'] = 'text/plain';
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ( xhrDetails.defer === false ) {
|
||||
xhrDetails.headers['content-length'] = `${xhrDetails.props.response.value}`.length;
|
||||
Object.defineProperties(xhrDetails.xhr, {
|
||||
readyState: { value: 4 },
|
||||
responseURL: { value: xhrDetails.url },
|
||||
status: { value: 200 },
|
||||
statusText: { value: 'OK' },
|
||||
});
|
||||
Object.defineProperties(xhrDetails.xhr, xhrDetails.props);
|
||||
return;
|
||||
}
|
||||
Promise.resolve(xhrText).then(( ) => xhrDetails).then(details => {
|
||||
Object.defineProperties(details.xhr, {
|
||||
readyState: { value: 1, configurable: true },
|
||||
responseURL: { value: xhrDetails.url },
|
||||
});
|
||||
safeDispatchEvent(details.xhr, 'readystatechange');
|
||||
return details;
|
||||
}).then(details => {
|
||||
xhrDetails.headers['content-length'] = `${details.props.response.value}`.length;
|
||||
Object.defineProperties(details.xhr, {
|
||||
readyState: { value: 2, configurable: true },
|
||||
status: { value: 200 },
|
||||
statusText: { value: 'OK' },
|
||||
});
|
||||
safeDispatchEvent(details.xhr, 'readystatechange');
|
||||
return details;
|
||||
}).then(details => {
|
||||
Object.defineProperties(details.xhr, {
|
||||
readyState: { value: 3, configurable: true },
|
||||
});
|
||||
Object.defineProperties(details.xhr, details.props);
|
||||
safeDispatchEvent(details.xhr, 'readystatechange');
|
||||
return details;
|
||||
}).then(details => {
|
||||
Object.defineProperties(details.xhr, {
|
||||
readyState: { value: 4 },
|
||||
});
|
||||
safeDispatchEvent(details.xhr, 'readystatechange');
|
||||
safeDispatchEvent(details.xhr, 'load');
|
||||
safeDispatchEvent(details.xhr, 'loadend');
|
||||
safe.uboLog(logPrefix, `Prevented with response:\n${details.xhr.response}`);
|
||||
});
|
||||
}
|
||||
getResponseHeader(headerName) {
|
||||
const xhrDetails = xhrInstances.get(this);
|
||||
if ( xhrDetails === undefined || this.readyState < this.HEADERS_RECEIVED ) {
|
||||
return super.getResponseHeader(headerName);
|
||||
}
|
||||
const value = xhrDetails.headers[headerName.toLowerCase()];
|
||||
if ( value !== undefined && value !== '' ) { return value; }
|
||||
return null;
|
||||
}
|
||||
getAllResponseHeaders() {
|
||||
const xhrDetails = xhrInstances.get(this);
|
||||
if ( xhrDetails === undefined || this.readyState < this.HEADERS_RECEIVED ) {
|
||||
return super.getAllResponseHeaders();
|
||||
}
|
||||
const out = [];
|
||||
for ( const [ name, value ] of Object.entries(xhrDetails.headers) ) {
|
||||
if ( !value ) { continue; }
|
||||
out.push(`${name}: ${value}`);
|
||||
}
|
||||
if ( out.length !== 0 ) { out.push(''); }
|
||||
return out.join('\r\n');
|
||||
}
|
||||
};
|
||||
self.XMLHttpRequest.prototype.open.toString = function() {
|
||||
return XHRBefore.open.toString();
|
||||
};
|
||||
self.XMLHttpRequest.prototype.send.toString = function() {
|
||||
return XHRBefore.send.toString();
|
||||
};
|
||||
self.XMLHttpRequest.prototype.getResponseHeader.toString = function() {
|
||||
return XHRBefore.getResponseHeader.toString();
|
||||
};
|
||||
self.XMLHttpRequest.prototype.getAllResponseHeaders.toString = function() {
|
||||
return XHRBefore.getAllResponseHeaders.toString();
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
|
|
@ -992,22 +802,6 @@ function webrtcIf(
|
|||
});
|
||||
}
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
builtinScriptlets.push({
|
||||
name: 'prevent-xhr.js',
|
||||
aliases: [
|
||||
'no-xhr-if.js',
|
||||
],
|
||||
fn: preventXhr,
|
||||
dependencies: [
|
||||
'prevent-xhr.fn',
|
||||
],
|
||||
});
|
||||
function preventXhr(...args) {
|
||||
return preventXhrFn(false, ...args);
|
||||
}
|
||||
|
||||
/**
|
||||
* @scriptlet prevent-window-open
|
||||
*
|
||||
|
|
@ -2337,25 +2131,6 @@ function trustedSuppressNativeMethod(
|
|||
});
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
*
|
||||
* Trusted version of prevent-xhr(), which allows the use of an arbitrary
|
||||
* string as response text.
|
||||
*
|
||||
* */
|
||||
|
||||
builtinScriptlets.push({
|
||||
name: 'trusted-prevent-xhr.js',
|
||||
requiresTrust: true,
|
||||
fn: trustedPreventXhr,
|
||||
dependencies: [
|
||||
'prevent-xhr.fn',
|
||||
],
|
||||
});
|
||||
function trustedPreventXhr(...args) {
|
||||
return preventXhrFn(true, ...args);
|
||||
}
|
||||
|
||||
/**
|
||||
* @trustedScriptlet trusted-prevent-dom-bypass
|
||||
*
|
||||
|
|
|
|||
Loading…
Reference in a new issue