Skip to content

Commit 4ce26b6

Browse files
committed
Add trusted-prevent-fetch scriptlet
Related feedback: uBlockOrigin/uBlock-discussions#915 (comment)
1 parent 2501eae commit 4ce26b6

File tree

3 files changed

+285
-186
lines changed

3 files changed

+285
-186
lines changed

src/js/resources/prevent-fetch.js

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
/*******************************************************************************
2+
3+
uBlock Origin - a comprehensive, efficient content blocker
4+
Copyright (C) 2019-present Raymond Hill
5+
6+
This program is free software: you can redistribute it and/or modify
7+
it under the terms of the GNU General Public License as published by
8+
the Free Software Foundation, either version 3 of the License, or
9+
(at your option) any later version.
10+
11+
This program is distributed in the hope that it will be useful,
12+
but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
GNU General Public License for more details.
15+
16+
You should have received a copy of the GNU General Public License
17+
along with this program. If not, see {http://www.gnu.org/licenses/}.
18+
19+
Home: https://github.com/gorhill/uBlock
20+
21+
*/
22+
23+
import { generateContentFn } from './utils.js';
24+
import { proxyApplyFn } from './proxy-apply.js';
25+
import { registerScriptlet } from './base.js';
26+
import { safeSelf } from './safe-self.js';
27+
28+
/******************************************************************************/
29+
30+
function preventFetchFn(
31+
trusted = false,
32+
propsToMatch = '',
33+
responseBody = '',
34+
responseType = ''
35+
) {
36+
const safe = safeSelf();
37+
const scriptletName = `${trusted ? 'trusted-' : ''}prevent-fetch`;
38+
const logPrefix = safe.makeLogPrefix(
39+
scriptletName,
40+
propsToMatch,
41+
responseBody,
42+
responseType
43+
);
44+
const needles = [];
45+
for ( const condition of safe.String_split.call(propsToMatch, /\s+/) ) {
46+
if ( condition === '' ) { continue; }
47+
const pos = condition.indexOf(':');
48+
let key, value;
49+
if ( pos !== -1 ) {
50+
key = condition.slice(0, pos);
51+
value = condition.slice(pos + 1);
52+
} else {
53+
key = 'url';
54+
value = condition;
55+
}
56+
needles.push({ key, pattern: safe.initPattern(value, { canNegate: true }) });
57+
}
58+
const validResponseProps = {
59+
ok: [ false, true ],
60+
statusText: [ '', 'Not Found' ],
61+
type: [ 'basic', 'cors', 'default', 'error', 'opaque' ],
62+
};
63+
const responseProps = {
64+
statusText: { value: 'OK' },
65+
};
66+
if ( /^\{.*\}$/.test(responseType) ) {
67+
try {
68+
Object.entries(JSON.parse(responseType)).forEach(([ p, v ]) => {
69+
if ( validResponseProps[p] === undefined ) { return; }
70+
if ( validResponseProps[p].includes(v) === false ) { return; }
71+
responseProps[p] = { value: v };
72+
});
73+
}
74+
catch { }
75+
} else if ( responseType !== '' ) {
76+
if ( validResponseProps.type.includes(responseType) ) {
77+
responseProps.type = { value: responseType };
78+
}
79+
}
80+
proxyApplyFn('fetch', function fetch(context) {
81+
const { callArgs } = context;
82+
const details = callArgs[0] instanceof self.Request
83+
? callArgs[0]
84+
: Object.assign({ url: callArgs[0] }, callArgs[1]);
85+
let proceed = true;
86+
try {
87+
const props = new Map();
88+
for ( const prop in details ) {
89+
let v = details[prop];
90+
if ( typeof v !== 'string' ) {
91+
try { v = safe.JSON_stringify(v); }
92+
catch { }
93+
}
94+
if ( typeof v !== 'string' ) { continue; }
95+
props.set(prop, v);
96+
}
97+
if ( safe.logLevel > 1 || propsToMatch === '' && responseBody === '' ) {
98+
const out = Array.from(props).map(a => `${a[0]}:${a[1]}`);
99+
safe.uboLog(logPrefix, `Called: ${out.join('\n')}`);
100+
}
101+
if ( propsToMatch === '' && responseBody === '' ) {
102+
return context.reflect();
103+
}
104+
proceed = needles.length === 0;
105+
for ( const { key, pattern } of needles ) {
106+
if (
107+
pattern.expect && props.has(key) === false ||
108+
safe.testPattern(pattern, props.get(key)) === false
109+
) {
110+
proceed = true;
111+
break;
112+
}
113+
}
114+
} catch {
115+
}
116+
if ( proceed ) {
117+
return context.reflect();
118+
}
119+
return Promise.resolve(generateContentFn(trusted, responseBody)).then(text => {
120+
safe.uboLog(logPrefix, `Prevented with response "${text}"`);
121+
const response = new Response(text, {
122+
headers: {
123+
'Content-Length': text.length,
124+
}
125+
});
126+
const props = Object.assign(
127+
{ url: { value: details.url } },
128+
responseProps
129+
);
130+
safe.Object_defineProperties(response, props);
131+
return response;
132+
});
133+
});
134+
}
135+
registerScriptlet(preventFetchFn, {
136+
name: 'prevent-fetch.fn',
137+
dependencies: [
138+
generateContentFn,
139+
proxyApplyFn,
140+
safeSelf,
141+
],
142+
});
143+
144+
/******************************************************************************/
145+
/**
146+
* @scriptlet prevent-fetch
147+
*
148+
* @description
149+
* Prevent a fetch() call from making a network request to a remote server.
150+
*
151+
* @param propsToMatch
152+
* The fetch arguments to match for the prevention to be triggered. The
153+
* untrusted flavor limits the realm of response to return to safe values.
154+
*
155+
* @param [responseBody]
156+
* Optional. The reponse to return when the prevention occurs.
157+
*
158+
* @param [responseType]
159+
* Optional. The response type to use when emitting a dummy response as a
160+
* result of the prevention.
161+
*
162+
* */
163+
164+
function preventFetch(...args) {
165+
preventFetchFn(false, ...args);
166+
}
167+
registerScriptlet(preventFetch, {
168+
name: 'prevent-fetch.js',
169+
aliases: [
170+
'no-fetch-if.js',
171+
],
172+
dependencies: [
173+
preventFetchFn,
174+
],
175+
});
176+
177+
/******************************************************************************/
178+
/**
179+
* @scriptlet trusted-prevent-fetch
180+
*
181+
* @description
182+
* Prevent a fetch() call from making a network request to a remote server.
183+
*
184+
* @param propsToMatch
185+
* The fetch arguments to match for the prevention to be triggered.
186+
*
187+
* @param [responseBody]
188+
* Optional. The reponse to return when the prevention occurs. The trusted
189+
* flavor allows to return any response.
190+
*
191+
* @param [responseType]
192+
* Optional. The response type to use when emitting a dummy response as a
193+
* result of the prevention.
194+
*
195+
* */
196+
197+
function trustedPreventFetch(...args) {
198+
preventFetchFn(true, ...args);
199+
}
200+
registerScriptlet(trustedPreventFetch, {
201+
name: 'trusted-prevent-fetch.js',
202+
requiresTrust: true,
203+
dependencies: [
204+
preventFetchFn,
205+
],
206+
});
207+
208+
/******************************************************************************/

0 commit comments

Comments
 (0)