Skip to content

Commit 1683944

Browse files
committed
Improve prevent-xhr scriptlet
1 parent 84434e0 commit 1683944

File tree

2 files changed

+271
-226
lines changed

2 files changed

+271
-226
lines changed

src/js/resources/prevent-xhr.js

Lines changed: 270 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,270 @@
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 {
24+
generateContentFn,
25+
matchObjectPropertiesFn,
26+
parsePropertiesToMatchFn,
27+
} from './utils.js';
28+
import { proxyApplyFn } from './proxy-apply.js';
29+
import { registerScriptlet } from './base.js';
30+
import { safeSelf } from './safe-self.js';
31+
32+
// Externally added to the private namespace in which scriptlets execute.
33+
/* global scriptletGlobals */
34+
35+
/******************************************************************************/
36+
37+
function preventXhrFn(
38+
trusted = false,
39+
propsToMatch = '',
40+
directive = ''
41+
) {
42+
if ( typeof propsToMatch !== 'string' ) { return; }
43+
const safe = safeSelf();
44+
const scriptletName = trusted ? 'trusted-prevent-xhr' : 'prevent-xhr';
45+
const logPrefix = safe.makeLogPrefix(scriptletName, propsToMatch, directive);
46+
const xhrInstances = new WeakMap();
47+
const propNeedles = parsePropertiesToMatchFn(propsToMatch, 'url');
48+
const warOrigin = scriptletGlobals.warOrigin;
49+
const safeDispatchEvent = (xhr, type) => {
50+
try {
51+
xhr.dispatchEvent(new Event(type));
52+
} catch {
53+
}
54+
};
55+
proxyApplyFn('XMLHttpRequest.prototype.open', function(context) {
56+
const { thisArg, callArgs } = context;
57+
xhrInstances.delete(thisArg);
58+
const [ method, url, ...args ] = callArgs;
59+
if ( warOrigin !== undefined && url.startsWith(warOrigin) ) {
60+
return context.reflect();
61+
}
62+
const haystack = { method, url };
63+
if ( propsToMatch === '' && directive === '' ) {
64+
safe.uboLog(logPrefix, `Called: ${safe.JSON_stringify(haystack, null, 2)}`);
65+
return context.reflect();
66+
}
67+
if ( matchObjectPropertiesFn(propNeedles, haystack) ) {
68+
const xhrDetails = Object.assign(haystack, {
69+
xhr: thisArg,
70+
defer: args.length === 0 || !!args[0],
71+
directive,
72+
headers: {
73+
'date': '',
74+
'content-type': '',
75+
'content-length': '',
76+
},
77+
url: haystack.url,
78+
props: {
79+
response: { value: '' },
80+
responseText: { value: '' },
81+
responseXML: { value: null },
82+
},
83+
});
84+
xhrInstances.set(thisArg, xhrDetails);
85+
}
86+
return context.reflect();
87+
});
88+
proxyApplyFn('XMLHttpRequest.prototype.send', function(context) {
89+
const { thisArg } = context;
90+
const xhrDetails = xhrInstances.get(thisArg);
91+
if ( xhrDetails === undefined ) {
92+
return context.reflect();
93+
}
94+
xhrDetails.headers['date'] = (new Date()).toUTCString();
95+
let xhrText = '';
96+
switch ( thisArg.responseType ) {
97+
case 'arraybuffer':
98+
xhrDetails.props.response.value = new ArrayBuffer(0);
99+
xhrDetails.headers['content-type'] = 'application/octet-stream';
100+
break;
101+
case 'blob':
102+
xhrDetails.props.response.value = new Blob([]);
103+
xhrDetails.headers['content-type'] = 'application/octet-stream';
104+
break;
105+
case 'document': {
106+
const parser = new DOMParser();
107+
const doc = parser.parseFromString('', 'text/html');
108+
xhrDetails.props.response.value = doc;
109+
xhrDetails.props.responseXML.value = doc;
110+
xhrDetails.headers['content-type'] = 'text/html';
111+
break;
112+
}
113+
case 'json':
114+
xhrDetails.props.response.value = {};
115+
xhrDetails.props.responseText.value = '{}';
116+
xhrDetails.headers['content-type'] = 'application/json';
117+
break;
118+
default: {
119+
if ( directive === '' ) { break; }
120+
xhrText = generateContentFn(trusted, xhrDetails.directive);
121+
if ( xhrText instanceof Promise ) {
122+
xhrText = xhrText.then(text => {
123+
xhrDetails.props.response.value = text;
124+
xhrDetails.props.responseText.value = text;
125+
});
126+
} else {
127+
xhrDetails.props.response.value = xhrText;
128+
xhrDetails.props.responseText.value = xhrText;
129+
}
130+
xhrDetails.headers['content-type'] = 'text/plain';
131+
break;
132+
}
133+
}
134+
if ( xhrDetails.defer === false ) {
135+
xhrDetails.headers['content-length'] = `${xhrDetails.props.response.value}`.length;
136+
Object.defineProperties(xhrDetails.xhr, {
137+
readyState: { value: 4 },
138+
responseURL: { value: xhrDetails.url },
139+
status: { value: 200 },
140+
statusText: { value: 'OK' },
141+
});
142+
Object.defineProperties(xhrDetails.xhr, xhrDetails.props);
143+
return;
144+
}
145+
Promise.resolve(xhrText).then(( ) => xhrDetails).then(details => {
146+
Object.defineProperties(details.xhr, {
147+
readyState: { value: 1, configurable: true },
148+
responseURL: { value: xhrDetails.url },
149+
});
150+
safeDispatchEvent(details.xhr, 'readystatechange');
151+
return details;
152+
}).then(details => {
153+
xhrDetails.headers['content-length'] = `${details.props.response.value}`.length;
154+
Object.defineProperties(details.xhr, {
155+
readyState: { value: 2, configurable: true },
156+
status: { value: 200 },
157+
statusText: { value: 'OK' },
158+
});
159+
safeDispatchEvent(details.xhr, 'readystatechange');
160+
return details;
161+
}).then(details => {
162+
Object.defineProperties(details.xhr, {
163+
readyState: { value: 3, configurable: true },
164+
});
165+
Object.defineProperties(details.xhr, details.props);
166+
safeDispatchEvent(details.xhr, 'readystatechange');
167+
return details;
168+
}).then(details => {
169+
Object.defineProperties(details.xhr, {
170+
readyState: { value: 4 },
171+
});
172+
safeDispatchEvent(details.xhr, 'readystatechange');
173+
safeDispatchEvent(details.xhr, 'load');
174+
safeDispatchEvent(details.xhr, 'loadend');
175+
safe.uboLog(logPrefix, `Prevented with response:\n${details.xhr.response}`);
176+
});
177+
});
178+
proxyApplyFn('XMLHttpRequest.prototype.getResponseHeader', function(context) {
179+
const { thisArg } = context;
180+
const xhrDetails = xhrInstances.get(thisArg);
181+
if ( xhrDetails === undefined || thisArg.readyState < thisArg.HEADERS_RECEIVED ) {
182+
return context.reflect();
183+
}
184+
const headerName = `${context.callArgs[0]}`;
185+
const value = xhrDetails.headers[headerName.toLowerCase()];
186+
if ( value !== undefined && value !== '' ) { return value; }
187+
return null;
188+
});
189+
proxyApplyFn('XMLHttpRequest.prototype.getAllResponseHeaders', function(context) {
190+
const { thisArg } = context;
191+
const xhrDetails = xhrInstances.get(thisArg);
192+
if ( xhrDetails === undefined || thisArg.readyState < thisArg.HEADERS_RECEIVED ) {
193+
return context.reflect();
194+
}
195+
const out = [];
196+
for ( const [ name, value ] of Object.entries(xhrDetails.headers) ) {
197+
if ( !value ) { continue; }
198+
out.push(`${name}: ${value}`);
199+
}
200+
if ( out.length !== 0 ) { out.push(''); }
201+
return out.join('\r\n');
202+
});
203+
}
204+
registerScriptlet(preventXhrFn, {
205+
name: 'prevent-xhr.fn',
206+
dependencies: [
207+
generateContentFn,
208+
matchObjectPropertiesFn,
209+
parsePropertiesToMatchFn,
210+
proxyApplyFn,
211+
safeSelf,
212+
],
213+
});
214+
215+
/******************************************************************************/
216+
/**
217+
* @scriptlet prevent-xhr
218+
*
219+
* @description
220+
* Prevent a XMLHttpRequest-baesed request from being sent to a remote server.
221+
*
222+
* @param propsToMatch
223+
* The fetch arguments to match for the prevention to be triggered. The
224+
* untrusted flavor limits the realm of response to return to safe values.
225+
*
226+
* @param [responseBody]
227+
* Optional. The reponse to return when the prevention occurs. The response
228+
* must be a safe constant value.
229+
*
230+
* */
231+
232+
function preventXhr(...args) {
233+
return preventXhrFn(false, ...args);
234+
}
235+
registerScriptlet(preventXhr, {
236+
name: 'prevent-xhr.js',
237+
aliases: [
238+
'no-xhr-if.js',
239+
],
240+
dependencies: [
241+
preventXhrFn,
242+
],
243+
});
244+
245+
/******************************************************************************/
246+
/**
247+
* @scriptlet trusted-prevent-xhr
248+
*
249+
* @description
250+
* Prevent a XMLHttpRequest-based request from being sent to a remote server.
251+
*
252+
* @param propsToMatch
253+
* The fetch arguments to match for the prevention to be triggered. The
254+
* untrusted flavor limits the realm of response to return to safe values.
255+
*
256+
* @param [responseBody]
257+
* Optional. The reponse to return when the prevention occurs. The trusted
258+
* version allows arbitrary response.
259+
*
260+
* */
261+
262+
function trustedPreventXhr(...args) {
263+
return preventXhrFn(true, ...args);
264+
}
265+
registerScriptlet(trustedPreventXhr, {
266+
name: 'trusted-prevent-xhr.js',
267+
dependencies: [
268+
preventXhrFn,
269+
],
270+
});

0 commit comments

Comments
 (0)