mirror of
https://github.com/gorhill/uBlock.git
synced 2026-03-11 09:04:36 +00:00
Add trusted-prevent-fetch scriptlet
Related feedback: https://github.com/uBlockOrigin/uBlock-discussions/discussions/915#discussioncomment-12077068
This commit is contained in:
parent
2501eae25f
commit
4ce26b63ff
3 changed files with 285 additions and 186 deletions
208
src/js/resources/prevent-fetch.js
Normal file
208
src/js/resources/prevent-fetch.js
Normal file
|
|
@ -0,0 +1,208 @@
|
|||
/*******************************************************************************
|
||||
|
||||
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 } from './utils.js';
|
||||
import { proxyApplyFn } from './proxy-apply.js';
|
||||
import { registerScriptlet } from './base.js';
|
||||
import { safeSelf } from './safe-self.js';
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
function preventFetchFn(
|
||||
trusted = false,
|
||||
propsToMatch = '',
|
||||
responseBody = '',
|
||||
responseType = ''
|
||||
) {
|
||||
const safe = safeSelf();
|
||||
const scriptletName = `${trusted ? 'trusted-' : ''}prevent-fetch`;
|
||||
const logPrefix = safe.makeLogPrefix(
|
||||
scriptletName,
|
||||
propsToMatch,
|
||||
responseBody,
|
||||
responseType
|
||||
);
|
||||
const needles = [];
|
||||
for ( const condition of safe.String_split.call(propsToMatch, /\s+/) ) {
|
||||
if ( condition === '' ) { continue; }
|
||||
const pos = condition.indexOf(':');
|
||||
let key, value;
|
||||
if ( pos !== -1 ) {
|
||||
key = condition.slice(0, pos);
|
||||
value = condition.slice(pos + 1);
|
||||
} else {
|
||||
key = 'url';
|
||||
value = condition;
|
||||
}
|
||||
needles.push({ key, pattern: safe.initPattern(value, { canNegate: true }) });
|
||||
}
|
||||
const validResponseProps = {
|
||||
ok: [ false, true ],
|
||||
statusText: [ '', 'Not Found' ],
|
||||
type: [ 'basic', 'cors', 'default', 'error', 'opaque' ],
|
||||
};
|
||||
const responseProps = {
|
||||
statusText: { value: 'OK' },
|
||||
};
|
||||
if ( /^\{.*\}$/.test(responseType) ) {
|
||||
try {
|
||||
Object.entries(JSON.parse(responseType)).forEach(([ p, v ]) => {
|
||||
if ( validResponseProps[p] === undefined ) { return; }
|
||||
if ( validResponseProps[p].includes(v) === false ) { return; }
|
||||
responseProps[p] = { value: v };
|
||||
});
|
||||
}
|
||||
catch { }
|
||||
} else if ( responseType !== '' ) {
|
||||
if ( validResponseProps.type.includes(responseType) ) {
|
||||
responseProps.type = { value: responseType };
|
||||
}
|
||||
}
|
||||
proxyApplyFn('fetch', function fetch(context) {
|
||||
const { callArgs } = context;
|
||||
const details = callArgs[0] instanceof self.Request
|
||||
? callArgs[0]
|
||||
: Object.assign({ url: callArgs[0] }, callArgs[1]);
|
||||
let proceed = true;
|
||||
try {
|
||||
const props = new Map();
|
||||
for ( const prop in details ) {
|
||||
let v = details[prop];
|
||||
if ( typeof v !== 'string' ) {
|
||||
try { v = safe.JSON_stringify(v); }
|
||||
catch { }
|
||||
}
|
||||
if ( typeof v !== 'string' ) { continue; }
|
||||
props.set(prop, v);
|
||||
}
|
||||
if ( safe.logLevel > 1 || propsToMatch === '' && responseBody === '' ) {
|
||||
const out = Array.from(props).map(a => `${a[0]}:${a[1]}`);
|
||||
safe.uboLog(logPrefix, `Called: ${out.join('\n')}`);
|
||||
}
|
||||
if ( propsToMatch === '' && responseBody === '' ) {
|
||||
return context.reflect();
|
||||
}
|
||||
proceed = needles.length === 0;
|
||||
for ( const { key, pattern } of needles ) {
|
||||
if (
|
||||
pattern.expect && props.has(key) === false ||
|
||||
safe.testPattern(pattern, props.get(key)) === false
|
||||
) {
|
||||
proceed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
}
|
||||
if ( proceed ) {
|
||||
return context.reflect();
|
||||
}
|
||||
return Promise.resolve(generateContentFn(trusted, responseBody)).then(text => {
|
||||
safe.uboLog(logPrefix, `Prevented with response "${text}"`);
|
||||
const response = new Response(text, {
|
||||
headers: {
|
||||
'Content-Length': text.length,
|
||||
}
|
||||
});
|
||||
const props = Object.assign(
|
||||
{ url: { value: details.url } },
|
||||
responseProps
|
||||
);
|
||||
safe.Object_defineProperties(response, props);
|
||||
return response;
|
||||
});
|
||||
});
|
||||
}
|
||||
registerScriptlet(preventFetchFn, {
|
||||
name: 'prevent-fetch.fn',
|
||||
dependencies: [
|
||||
generateContentFn,
|
||||
proxyApplyFn,
|
||||
safeSelf,
|
||||
],
|
||||
});
|
||||
|
||||
/******************************************************************************/
|
||||
/**
|
||||
* @scriptlet prevent-fetch
|
||||
*
|
||||
* @description
|
||||
* Prevent a fetch() call from making a network request 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.
|
||||
*
|
||||
* @param [responseType]
|
||||
* Optional. The response type to use when emitting a dummy response as a
|
||||
* result of the prevention.
|
||||
*
|
||||
* */
|
||||
|
||||
function preventFetch(...args) {
|
||||
preventFetchFn(false, ...args);
|
||||
}
|
||||
registerScriptlet(preventFetch, {
|
||||
name: 'prevent-fetch.js',
|
||||
aliases: [
|
||||
'no-fetch-if.js',
|
||||
],
|
||||
dependencies: [
|
||||
preventFetchFn,
|
||||
],
|
||||
});
|
||||
|
||||
/******************************************************************************/
|
||||
/**
|
||||
* @scriptlet trusted-prevent-fetch
|
||||
*
|
||||
* @description
|
||||
* Prevent a fetch() call from making a network request to a remote server.
|
||||
*
|
||||
* @param propsToMatch
|
||||
* The fetch arguments to match for the prevention to be triggered.
|
||||
*
|
||||
* @param [responseBody]
|
||||
* Optional. The reponse to return when the prevention occurs. The trusted
|
||||
* flavor allows to return any response.
|
||||
*
|
||||
* @param [responseType]
|
||||
* Optional. The response type to use when emitting a dummy response as a
|
||||
* result of the prevention.
|
||||
*
|
||||
* */
|
||||
|
||||
function trustedPreventFetch(...args) {
|
||||
preventFetchFn(true, ...args);
|
||||
}
|
||||
registerScriptlet(trustedPreventFetch, {
|
||||
name: 'trusted-prevent-fetch.js',
|
||||
requiresTrust: true,
|
||||
dependencies: [
|
||||
preventFetchFn,
|
||||
],
|
||||
});
|
||||
|
||||
/******************************************************************************/
|
||||
|
|
@ -26,12 +26,14 @@ import './json-edit.js';
|
|||
import './json-prune.js';
|
||||
import './noeval.js';
|
||||
import './object-prune.js';
|
||||
import './prevent-fetch.js';
|
||||
import './prevent-innerHTML.js';
|
||||
import './prevent-settimeout.js';
|
||||
import './replace-argument.js';
|
||||
import './spoof-css.js';
|
||||
|
||||
import {
|
||||
generateContentFn,
|
||||
getExceptionTokenFn,
|
||||
getRandomTokenFn,
|
||||
matchObjectPropertiesFn,
|
||||
|
|
@ -73,79 +75,6 @@ function shouldDebug(details) {
|
|||
|
||||
/******************************************************************************/
|
||||
|
||||
// Reference:
|
||||
// https://github.com/AdguardTeam/Scriptlets/blob/master/wiki/about-scriptlets.md#prevent-xhr
|
||||
//
|
||||
// Added `trusted` argument to allow for returning arbitrary text. Can only
|
||||
// be used through scriptlets requiring trusted source.
|
||||
|
||||
builtinScriptlets.push({
|
||||
name: 'generate-content.fn',
|
||||
fn: generateContentFn,
|
||||
dependencies: [
|
||||
'safe-self.fn',
|
||||
],
|
||||
});
|
||||
function generateContentFn(trusted, directive) {
|
||||
const safe = safeSelf();
|
||||
const randomize = len => {
|
||||
const chunks = [];
|
||||
let textSize = 0;
|
||||
do {
|
||||
const s = safe.Math_random().toString(36).slice(2);
|
||||
chunks.push(s);
|
||||
textSize += s.length;
|
||||
}
|
||||
while ( textSize < len );
|
||||
return chunks.join(' ').slice(0, len);
|
||||
};
|
||||
if ( directive === 'true' ) {
|
||||
return randomize(10);
|
||||
}
|
||||
if ( directive === 'emptyObj' ) {
|
||||
return '{}';
|
||||
}
|
||||
if ( directive === 'emptyArr' ) {
|
||||
return '[]';
|
||||
}
|
||||
if ( directive === 'emptyStr' ) {
|
||||
return '';
|
||||
}
|
||||
if ( directive.startsWith('length:') ) {
|
||||
const match = /^length:(\d+)(?:-(\d+))?$/.exec(directive);
|
||||
if ( match === null ) { return ''; }
|
||||
const min = parseInt(match[1], 10);
|
||||
const extent = safe.Math_max(parseInt(match[2], 10) || 0, min) - min;
|
||||
const len = safe.Math_min(min + extent * safe.Math_random(), 500000);
|
||||
return randomize(len | 0);
|
||||
}
|
||||
if ( directive.startsWith('war:') ) {
|
||||
if ( scriptletGlobals.warOrigin === undefined ) { return ''; }
|
||||
return new Promise(resolve => {
|
||||
const warOrigin = scriptletGlobals.warOrigin;
|
||||
const warName = directive.slice(4);
|
||||
const fullpath = [ warOrigin, '/', warName ];
|
||||
const warSecret = scriptletGlobals.warSecret;
|
||||
if ( warSecret !== undefined ) {
|
||||
fullpath.push('?secret=', warSecret);
|
||||
}
|
||||
const warXHR = new safe.XMLHttpRequest();
|
||||
warXHR.responseType = 'text';
|
||||
warXHR.onloadend = ev => {
|
||||
resolve(ev.target.responseText || '');
|
||||
};
|
||||
warXHR.open('GET', fullpath.join(''));
|
||||
warXHR.send();
|
||||
}).catch(( ) => '');
|
||||
}
|
||||
if ( trusted ) {
|
||||
return directive;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
builtinScriptlets.push({
|
||||
name: 'abort-current-script-core.fn',
|
||||
fn: abortCurrentScriptCore,
|
||||
|
|
@ -981,119 +910,6 @@ function adjustSetTimeout(
|
|||
|
||||
/******************************************************************************/
|
||||
|
||||
builtinScriptlets.push({
|
||||
name: 'prevent-fetch.js',
|
||||
aliases: [
|
||||
'no-fetch-if.js',
|
||||
],
|
||||
fn: noFetchIf,
|
||||
dependencies: [
|
||||
'generate-content.fn',
|
||||
'proxy-apply.fn',
|
||||
'safe-self.fn',
|
||||
],
|
||||
});
|
||||
function noFetchIf(
|
||||
propsToMatch = '',
|
||||
responseBody = '',
|
||||
responseType = ''
|
||||
) {
|
||||
const safe = safeSelf();
|
||||
const logPrefix = safe.makeLogPrefix('prevent-fetch', propsToMatch, responseBody, responseType);
|
||||
const needles = [];
|
||||
for ( const condition of safe.String_split.call(propsToMatch, /\s+/) ) {
|
||||
if ( condition === '' ) { continue; }
|
||||
const pos = condition.indexOf(':');
|
||||
let key, value;
|
||||
if ( pos !== -1 ) {
|
||||
key = condition.slice(0, pos);
|
||||
value = condition.slice(pos + 1);
|
||||
} else {
|
||||
key = 'url';
|
||||
value = condition;
|
||||
}
|
||||
needles.push({ key, pattern: safe.initPattern(value, { canNegate: true }) });
|
||||
}
|
||||
const validResponseProps = {
|
||||
ok: [ false, true ],
|
||||
statusText: [ '', 'Not Found' ],
|
||||
type: [ 'basic', 'cors', 'default', 'error', 'opaque' ],
|
||||
};
|
||||
const responseProps = {
|
||||
statusText: { value: 'OK' },
|
||||
};
|
||||
if ( /^\{.*\}$/.test(responseType) ) {
|
||||
try {
|
||||
Object.entries(JSON.parse(responseType)).forEach(([ p, v ]) => {
|
||||
if ( validResponseProps[p] === undefined ) { return; }
|
||||
if ( validResponseProps[p].includes(v) === false ) { return; }
|
||||
responseProps[p] = { value: v };
|
||||
});
|
||||
}
|
||||
catch { }
|
||||
} else if ( responseType !== '' ) {
|
||||
if ( validResponseProps.type.includes(responseType) ) {
|
||||
responseProps.type = { value: responseType };
|
||||
}
|
||||
}
|
||||
proxyApplyFn('fetch', function fetch(context) {
|
||||
const { callArgs } = context;
|
||||
const details = callArgs[0] instanceof self.Request
|
||||
? callArgs[0]
|
||||
: Object.assign({ url: callArgs[0] }, callArgs[1]);
|
||||
let proceed = true;
|
||||
try {
|
||||
const props = new Map();
|
||||
for ( const prop in details ) {
|
||||
let v = details[prop];
|
||||
if ( typeof v !== 'string' ) {
|
||||
try { v = safe.JSON_stringify(v); }
|
||||
catch { }
|
||||
}
|
||||
if ( typeof v !== 'string' ) { continue; }
|
||||
props.set(prop, v);
|
||||
}
|
||||
if ( safe.logLevel > 1 || propsToMatch === '' && responseBody === '' ) {
|
||||
const out = Array.from(props).map(a => `${a[0]}:${a[1]}`);
|
||||
safe.uboLog(logPrefix, `Called: ${out.join('\n')}`);
|
||||
}
|
||||
if ( propsToMatch === '' && responseBody === '' ) {
|
||||
return context.reflect();
|
||||
}
|
||||
proceed = needles.length === 0;
|
||||
for ( const { key, pattern } of needles ) {
|
||||
if (
|
||||
pattern.expect && props.has(key) === false ||
|
||||
safe.testPattern(pattern, props.get(key)) === false
|
||||
) {
|
||||
proceed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
}
|
||||
if ( proceed ) {
|
||||
return context.reflect();
|
||||
}
|
||||
return Promise.resolve(generateContentFn(false, responseBody)).then(text => {
|
||||
safe.uboLog(logPrefix, `Prevented with response "${text}"`);
|
||||
const response = new Response(text, {
|
||||
headers: {
|
||||
'Content-Length': text.length,
|
||||
}
|
||||
});
|
||||
const props = Object.assign(
|
||||
{ url: { value: details.url } },
|
||||
responseProps
|
||||
);
|
||||
safe.Object_defineProperties(response, props);
|
||||
return response;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
builtinScriptlets.push({
|
||||
name: 'prevent-refresh.js',
|
||||
aliases: [
|
||||
|
|
|
|||
|
|
@ -23,6 +23,9 @@
|
|||
import { registerScriptlet } from './base.js';
|
||||
import { safeSelf } from './safe-self.js';
|
||||
|
||||
// Externally added to the private namespace in which scriptlets execute.
|
||||
/* global scriptletGlobals */
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
export function getRandomTokenFn() {
|
||||
|
|
@ -115,3 +118,75 @@ registerScriptlet(matchObjectPropertiesFn, {
|
|||
});
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
// Reference:
|
||||
// https://github.com/AdguardTeam/Scriptlets/blob/master/wiki/about-scriptlets.md#prevent-xhr
|
||||
//
|
||||
// Added `trusted` argument to allow for returning arbitrary text. Can only
|
||||
// be used through scriptlets requiring trusted source.
|
||||
|
||||
export function generateContentFn(trusted, directive) {
|
||||
const safe = safeSelf();
|
||||
const randomize = len => {
|
||||
const chunks = [];
|
||||
let textSize = 0;
|
||||
do {
|
||||
const s = safe.Math_random().toString(36).slice(2);
|
||||
chunks.push(s);
|
||||
textSize += s.length;
|
||||
}
|
||||
while ( textSize < len );
|
||||
return chunks.join(' ').slice(0, len);
|
||||
};
|
||||
if ( directive === 'true' ) {
|
||||
return randomize(10);
|
||||
}
|
||||
if ( directive === 'emptyObj' ) {
|
||||
return '{}';
|
||||
}
|
||||
if ( directive === 'emptyArr' ) {
|
||||
return '[]';
|
||||
}
|
||||
if ( directive === 'emptyStr' ) {
|
||||
return '';
|
||||
}
|
||||
if ( directive.startsWith('length:') ) {
|
||||
const match = /^length:(\d+)(?:-(\d+))?$/.exec(directive);
|
||||
if ( match === null ) { return ''; }
|
||||
const min = parseInt(match[1], 10);
|
||||
const extent = safe.Math_max(parseInt(match[2], 10) || 0, min) - min;
|
||||
const len = safe.Math_min(min + extent * safe.Math_random(), 500000);
|
||||
return randomize(len | 0);
|
||||
}
|
||||
if ( directive.startsWith('war:') ) {
|
||||
if ( scriptletGlobals.warOrigin === undefined ) { return ''; }
|
||||
return new Promise(resolve => {
|
||||
const warOrigin = scriptletGlobals.warOrigin;
|
||||
const warName = directive.slice(4);
|
||||
const fullpath = [ warOrigin, '/', warName ];
|
||||
const warSecret = scriptletGlobals.warSecret;
|
||||
if ( warSecret !== undefined ) {
|
||||
fullpath.push('?secret=', warSecret);
|
||||
}
|
||||
const warXHR = new safe.XMLHttpRequest();
|
||||
warXHR.responseType = 'text';
|
||||
warXHR.onloadend = ev => {
|
||||
resolve(ev.target.responseText || '');
|
||||
};
|
||||
warXHR.open('GET', fullpath.join(''));
|
||||
warXHR.send();
|
||||
}).catch(( ) => '');
|
||||
}
|
||||
if ( trusted ) {
|
||||
return directive;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
registerScriptlet(generateContentFn, {
|
||||
name: 'generate-content.fn',
|
||||
dependencies: [
|
||||
safeSelf,
|
||||
],
|
||||
});
|
||||
|
||||
/******************************************************************************/
|
||||
|
|
|
|||
Loading…
Reference in a new issue