Skip to content

Commit

Permalink
modify original response by prevent-fetch
Browse files Browse the repository at this point in the history
  • Loading branch information
slavaleleka committed May 10, 2023
1 parent abab623 commit 531ea12
Show file tree
Hide file tree
Showing 4 changed files with 156 additions and 12 deletions.
1 change: 1 addition & 0 deletions src/helpers/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export * from './open-shadow-dom-utils';
export * from './prevent-utils';
export * from './prevent-window-open-utils';
export * from './regexp-utils';
export * from './response-utils';
export * from './request-utils';
export * from './storage-utils';
export * from './string-utils';
Expand Down
40 changes: 40 additions & 0 deletions src/helpers/response-utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/**
* Modifies original response with the given replacement data.
*
* @param {Response} origResponse Original response.
* @param {Object} replacement Replacement data for response with possible keys:
* - `body`: optional, string, default to '{}';
* - `type`: optional, string, original response type is used if not specified.
*
* @returns {Response} Modified response.
*/
export const modifyResponse = (
origResponse,
replacement = {
body: '{}',
},
) => {
const headers = {};
origResponse?.headers?.forEach((value, key) => {
headers[key] = value;
});

const modifiedResponse = new Response(replacement.body, {
status: origResponse.status,
statusText: origResponse.statusText,
headers,
});

// Mock response url and type to avoid adblocker detection
// https://github.com/AdguardTeam/Scriptlets/issues/216
Object.defineProperties(modifiedResponse, {
url: {
value: origResponse.url,
},
type: {
value: replacement.type || origResponse.type,
},
});

return modifiedResponse;
};
38 changes: 28 additions & 10 deletions src/scriptlets/prevent-fetch.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
noopPromiseResolve,
matchRequestProps,
logMessage,
modifyResponse,
// following helpers should be imported and injected
// because they are used by helpers above
toRegExp,
Expand All @@ -24,7 +25,7 @@ import {
/**
* @scriptlet prevent-fetch
* @description
* Prevents `fetch` calls if **all** given parameters match
* Prevents `fetch` calls if **all** given parameters match.
*
* Related UBO scriptlet:
* https://github.com/gorhill/uBlock/wiki/Resources-Library#no-fetch-ifjs-
Expand All @@ -35,14 +36,18 @@ import {
* ```
*
* - `propsToMatch` — optional, string of space-separated properties to match; possible props:
* - string or regular expression for matching the URL passed to fetch call; empty string, wildcard `*` or invalid regular expression will match all fetch calls
* - string or regular expression for matching the URL passed to fetch call;
* empty string, wildcard `*` or invalid regular expression will match all fetch calls
* - colon-separated pairs `name:value` where
* - `name` is [`init` option name](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch#parameters)
* - `value` is string or regular expression for matching the value of the option passed to fetch call; invalid regular expression will cause any value matching
* - `responseBody` — optional, string for defining response body value, defaults to `emptyObj`. Possible values:
* - `value` is string or regular expression for matching the value of the option passed to fetch call;
* invalid regular expression will cause any value matching
* - `responseBody` — optional, string for defining response body value,
* defaults to `emptyObj`. Possible values:
* - `emptyObj` — empty object
* - `emptyArr` — empty array
* - `responseType` — optional, string for defining response type, defaults to `default`. Possible values:
* - `responseType` — optional, string for defining response type,
* original response type is used if not specified. Possible values:
* - `default`
* - `opaque`
*
Expand Down Expand Up @@ -92,7 +97,7 @@ import {
* ```
*/
/* eslint-enable max-len */
export function preventFetch(source, propsToMatch, responseBody = 'emptyObj', responseType = 'default') {
export function preventFetch(source, propsToMatch, responseBody = 'emptyObj', responseType) {
// do nothing if browser does not support fetch or Proxy (e.g. Internet Explorer)
// https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy
Expand All @@ -108,12 +113,15 @@ export function preventFetch(source, propsToMatch, responseBody = 'emptyObj', re
} else if (responseBody === 'emptyArr') {
strResponseBody = '[]';
} else {
logMessage(source, `Invalid responseBody parameter: ${responseBody}`);
return;
}

// Skip disallowed response types
if (!(responseType === 'default' || responseType === 'opaque')) {
logMessage(source, `Invalid parameter: ${responseType}`);
// Skip disallowed response types,
// specified responseType has limited list of possible values
if (typeof responseType !== 'undefined'
&& !(responseType === 'default' || responseType === 'opaque')) {
logMessage(source, `Invalid responseType parameter: ${responseType}`);
return;
}

Expand All @@ -130,7 +138,16 @@ export function preventFetch(source, propsToMatch, responseBody = 'emptyObj', re

if (shouldPrevent) {
hit(source);
return noopPromiseResolve(strResponseBody, fetchData.url, responseType);
return Reflect.apply(target, thisArg, args)
.then((origResponse) => {
return modifyResponse(
origResponse,
{
body: strResponseBody,
type: responseType,
},
);
});
}

return Reflect.apply(target, thisArg, args);
Expand Down Expand Up @@ -158,6 +175,7 @@ preventFetch.injections = [
noopPromiseResolve,
matchRequestProps,
logMessage,
modifyResponse,
toRegExp,
isValidStrPattern,
escapeRegExp,
Expand Down
89 changes: 87 additions & 2 deletions tests/scriptlets/prevent-fetch.test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
/* eslint-disable no-underscore-dangle, no-console */
import { runScriptlet, clearGlobalProps } from '../helpers';
import { startsWith } from '../../src/helpers/string-utils';
import { isEmptyObject } from '../../src/helpers/object-utils';

const { test, module } = QUnit;
Expand Down Expand Up @@ -71,7 +70,7 @@ if (!isSupported) {
return;
}
const EXPECTED_LOG_STR_START = `${name}: fetch( url:"${INPUT_JSON_PATH}" method:"${TEST_METHOD}"`;
assert.ok(startsWith(input, EXPECTED_LOG_STR_START), 'console.hit input');
assert.ok(input.startsWith(EXPECTED_LOG_STR_START), 'console.hit input');
};

// no args -> just logging, no preventing
Expand Down Expand Up @@ -129,6 +128,7 @@ if (!isSupported) {
assert.ok(isEmptyObject(parsedData1), 'Response is mocked');
assert.strictEqual(window.hit, 'FIRED', 'hit function fired');
done();
// remove 'hit' property for following checking
clearGlobalProps('hit');

const response2 = await fetch(inputRequest2);
Expand Down Expand Up @@ -275,6 +275,23 @@ if (!isSupported) {
done();
});

test('simple fetch - valid response type', async (assert) => {
const OPAQUE_RESPONSE_TYPE = 'opaque';
const INPUT_JSON_PATH = `${FETCH_OBJECTS_PATH}/test01.json`;
const init = {
method: 'GET',
};

runScriptlet(name, ['*', '', OPAQUE_RESPONSE_TYPE]);
const done = assert.async();

const response = await fetch(INPUT_JSON_PATH, init);

assert.strictEqual(response.type, OPAQUE_RESPONSE_TYPE, 'Response type is set');
assert.strictEqual(window.hit, 'FIRED', 'hit function fired');
done();
});

test('simple fetch - invalid response type', async (assert) => {
const INVALID_RESPONSE_TYPE = 'invalid_type';
const BASIC_RESPONSE_TYPE = 'basic';
Expand All @@ -301,4 +318,72 @@ if (!isSupported) {
assert.strictEqual(window.hit, undefined, 'hit function fired');
done();
});

test('simple fetch -- all original response properties are not modified', async (assert) => {
const TEST_FILE_NAME = 'test01.json';
const INPUT_JSON_PATH_1 = `${FETCH_OBJECTS_PATH}/${TEST_FILE_NAME}`;
const inputRequest1 = new Request(INPUT_JSON_PATH_1);
const done = assert.async(1);

runScriptlet(name, ['*']);

const response = await fetch(inputRequest1);

/**
* Previously, only one header was present in the returned response
* which was `content-type: text/plain;charset=UTF-8`.
* Since we are not modifying the headers, we expect to receive more than one header.
* We cannot check the exact headers and their values
* because the response may contain different headers
* depending on whether the tests are run in Node or in a browser.
*/
let headersCount = 0;
// eslint-disable-next-line no-unused-vars
for (const key of response.headers.keys()) {
headersCount += 1;
}

assert.strictEqual(response.type, 'basic', 'response type is "basic" by default, not modified');
assert.true(response.url.includes(TEST_FILE_NAME), 'response url not modified');
assert.true(headersCount > 1, 'original headers not modified');

const responseJsonData = await response.json();
assert.ok(isEmptyObject(responseJsonData), 'response data is mocked');
assert.strictEqual(window.hit, 'FIRED', 'hit function fired');
done();
});

test('simple fetch -- original response properties are not modified except type', async (assert) => {
const TEST_FILE_NAME = 'test01.json';
const TEST_RESPONSE_TYPE = 'opaque';
const INPUT_JSON_PATH_1 = `${FETCH_OBJECTS_PATH}/${TEST_FILE_NAME}`;
const inputRequest1 = new Request(INPUT_JSON_PATH_1);
const done = assert.async(1);

runScriptlet(name, ['*', 'emptyArr', TEST_RESPONSE_TYPE]);

const response = await fetch(inputRequest1);

let headersCount = 0;
// eslint-disable-next-line no-unused-vars
for (const key of response.headers.keys()) {
headersCount += 1;
}

assert.strictEqual(
response.type,
TEST_RESPONSE_TYPE,
`response type is modified, equals to ${TEST_RESPONSE_TYPE}`,
);
assert.true(response.url.includes(TEST_FILE_NAME), 'response url not modified');
assert.true(headersCount > 1, 'original headers not modified');

const responseJsonData = await response.json();
assert.ok(
Array.isArray(responseJsonData) && responseJsonData.length === 0,
'response data is an empty array',
);
assert.strictEqual(window.hit, 'FIRED', 'hit function fired');
done();
});
}

0 comments on commit 531ea12

Please sign in to comment.