Skip to content

Commit

Permalink
improve prevent-fetch — return original url in response #216 AG-14514
Browse files Browse the repository at this point in the history
Merge in ADGUARD-FILTERS/scriptlets from fix/AG-14514 to release/v1.7

Squashed commit of the following:

commit 53cfe6a
Merge: 1a0d9c5 c01dd65
Author: Stanislav A <s.atroschenko@adguard.com>
Date:   Tue Oct 25 16:06:50 2022 +0300

    Merge branch 'release/v1.7' into fix/AG-14514

commit 1a0d9c5
Author: Stanislav A <s.atroschenko@adguard.com>
Date:   Fri Oct 21 14:19:56 2022 +0300

    fix tests

commit 2959639
Author: Stanislav A <s.atroschenko@adguard.com>
Date:   Fri Oct 21 14:14:57 2022 +0300

    remove redundant whitespaces in tests assertions

commit b39f05c
Author: Stanislav A <s.atroschenko@adguard.com>
Date:   Fri Oct 21 14:13:04 2022 +0300

    update build-tests: move helpers to MULTIPLE_TEST_FILES_DIR

commit c35d374
Author: Stanislav A <s.atroschenko@adguard.com>
Date:   Thu Oct 20 13:14:15 2022 +0300

    redo helpers tests file structure

commit 5a659c2
Author: Stanislav A <s.atroschenko@adguard.com>
Date:   Thu Oct 20 12:56:33 2022 +0300

    fix imports

commit 3e3b940
Author: Stanislav A <s.atroschenko@adguard.com>
Date:   Thu Oct 20 12:46:47 2022 +0300

    improve tests

commit b9f9c1e
Author: Stanislav A <s.atroschenko@adguard.com>
Date:   Wed Oct 19 20:09:20 2022 +0300

    revert index.test typo

commit bd76d2c
Author: Stanislav A <s.atroschenko@adguard.com>
Date:   Wed Oct 19 20:07:36 2022 +0300

    split helpers tests to separate files and fix typo in noopPromiseResolve

commit 1bc6e81
Author: Stanislav A <s.atroschenko@adguard.com>
Date:   Wed Oct 19 19:40:22 2022 +0300

    add arg parser test case

commit 6509f76
Author: Stanislav A <s.atroschenko@adguard.com>
Date:   Wed Oct 19 19:23:36 2022 +0300

    fix response type restrictions and add tests

commit 44ed1ad
Author: Stanislav A <s.atroschenko@adguard.com>
Date:   Wed Oct 19 17:01:43 2022 +0300

    only allow opaque for responseType arg

commit 562ad11
Author: Stanislav A <s.atroschenko@adguard.com>
Date:   Wed Oct 19 14:24:11 2022 +0300

    fix typo and helper param description

commit a9f9cdf
Author: Stanislav A <s.atroschenko@adguard.com>
Date:   Wed Oct 19 14:05:28 2022 +0300

    fix match prop parser and add tests

commit d9c0c53
Author: Stanislav A <s.atroschenko@adguard.com>
Date:   Wed Oct 19 13:04:25 2022 +0300

    add response type mock for noopPromiseResolve

commit 2f886f0
Author: Stanislav A <s.atroschenko@adguard.com>
Date:   Fri Oct 14 15:09:04 2022 +0300

    fix url parsing

commit fac3e17
Author: Stanislav A <s.atroschenko@adguard.com>
Date:   Thu Oct 13 15:09:42 2022 +0300

    remove defineProperty in favor of prop

commit b9be4e5
Author: Stanislav A <s.atroschenko@adguard.com>
Date:   Tue Oct 11 20:49:11 2022 +0300

    improve helper comments

commit 9fa0ab1
Author: Stanislav A <s.atroschenko@adguard.com>
Date:   Mon Oct 10 17:34:00 2022 +0300

    improve prevent-fetch
  • Loading branch information
stanislav-atr committed Oct 25, 2022
1 parent c01dd65 commit d435aac
Show file tree
Hide file tree
Showing 11 changed files with 360 additions and 122 deletions.
2 changes: 1 addition & 1 deletion scripts/build-tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,10 +81,10 @@ const getTestConfigs = () => {
const MULTIPLE_TEST_FILES_DIRS = [
'scriptlets',
'redirects',
'helpers',
];
const ONE_TEST_FILE_DIRS = [
'lib-tests',
'helpers',
];

const multipleFilesConfigs = MULTIPLE_TEST_FILES_DIRS
Expand Down
15 changes: 12 additions & 3 deletions src/helpers/noop.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,11 @@ export const noopObject = () => ({});
export const noopPromiseReject = () => Promise.reject(); // eslint-disable-line compat/compat

/**
* Returns Promise object that is resolved with a response
* @param {string} [responseBody='{}'] value of response body
* Returns Promise object that is resolved value of response body
* @param {string} [url=''] value of response url to set on response object
* @param {string} [response='default'] value of response type to set on response object
*/
export const noopPromiseResolve = (responseBody = '{}') => {
export const noopPromiseResolve = (responseBody = '{}', responseUrl = '', responseType = 'default') => {
if (typeof Response === 'undefined') {
return;
}
Expand All @@ -65,6 +66,14 @@ export const noopPromiseResolve = (responseBody = '{}') => {
status: 200,
statusText: 'OK',
});

// Mock response' url & type to avoid adb checks
// https://github.com/AdguardTeam/Scriptlets/issues/216
Object.defineProperties(response, {
url: { value: responseUrl },
type: { value: responseType },
});

// eslint-disable-next-line compat/compat, consistent-return
return Promise.resolve(response);
};
30 changes: 26 additions & 4 deletions src/helpers/request-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,18 +86,40 @@ export const getXhrData = (method, url, async, user, password) => {
export const parseMatchProps = (propsToMatchStr) => {
const PROPS_DIVIDER = ' ';
const PAIRS_MARKER = ':';
const LEGAL_MATCH_PROPS = [
'method',
'url',
'headers',
'body',
'mode',
'credentials',
'cache',
'redirect',
'referrer',
'referrerPolicy',
'integrity',
'keepalive',
'signal',
'async',
];

const propsObj = {};
const props = propsToMatchStr.split(PROPS_DIVIDER);

props.forEach((prop) => {
const dividerInd = prop.indexOf(PAIRS_MARKER);
if (dividerInd === -1) {
propsObj.url = prop;
} else {
const key = prop.slice(0, dividerInd);

const key = prop.slice(0, dividerInd);
const hasLegalMatchProp = LEGAL_MATCH_PROPS.indexOf(key) !== -1;

if (hasLegalMatchProp) {
const value = prop.slice(dividerInd + 1);
propsObj[key] = value;
} else {
// Escape multiple colons in prop
// i.e regex value and/or url with protocol specified, with or without 'url:' match prop
// https://github.com/AdguardTeam/Scriptlets/issues/216#issuecomment-1178591463
propsObj.url = prop;
}
});

Expand Down
19 changes: 15 additions & 4 deletions src/scriptlets/prevent-fetch.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import {
*
* **Syntax**
* ```
* example.org#%#//scriptlet('prevent-fetch'[, propsToMatch[, responseBody]])
* example.org#%#//scriptlet('prevent-fetch'[, propsToMatch[, responseBody[, responseType]]])
* ```
*
* - `propsToMatch` - optional, string of space-separated properties to match; possible props:
Expand All @@ -41,8 +41,12 @@ import {
* - 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:
* - default
* - opaque
*
* > Usage with no arguments will log fetch calls to browser console;
* which is useful for debugging but permitted for production filter lists.
* which is useful for debugging but not permitted for production filter lists.
*
* **Examples**
* 1. Log all fetch calls
Expand Down Expand Up @@ -82,7 +86,7 @@ import {
* ```
*/
/* eslint-enable max-len */
export function preventFetch(source, propsToMatch, responseBody = 'emptyObj') {
export function preventFetch(source, propsToMatch, responseBody = 'emptyObj', responseType = 'default') {
// 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 @@ -101,6 +105,13 @@ export function preventFetch(source, propsToMatch, responseBody = 'emptyObj') {
return;
}

// Skip disallowed response types
if (!(responseType === 'default' || responseType === 'opaque')) {
// eslint-disable-next-line no-console
console.log(`Invalid parameter: ${responseType}`);
return;
}

const handlerWrapper = (target, thisArg, args) => {
let shouldPrevent = false;
const fetchData = getFetchData(args);
Expand Down Expand Up @@ -131,7 +142,7 @@ export function preventFetch(source, propsToMatch, responseBody = 'emptyObj') {

if (shouldPrevent) {
hit(source);
return noopPromiseResolve(strResponseBody);
return noopPromiseResolve(strResponseBody, fetchData.url, responseType);
}

return Reflect.apply(target, thisArg, args);
Expand Down
87 changes: 87 additions & 0 deletions tests/helpers/fetch-utils.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { parseMatchProps } from '../../src/helpers';

const { test, module } = QUnit;
const name = 'scriptlets-redirects helpers';

module(name);

const GET_METHOD = 'GET';
const METHOD_PROP = 'method';
const URL_PROP = 'url';

const URL1 = 'example.com';
const URL2 = 'http://example.com';
const URL3 = '/^https?://example.org/';
const URL4 = '/^https?://example.org/section#user:45/comments/';

test('Test parseMatchProps with different url props, simple input', (assert) => {
assert.strictEqual(parseMatchProps(URL1).url, URL1, 'No url match prop, no protocol, not regexp');
assert.strictEqual(parseMatchProps(`url:${URL1}`).url, URL1, 'url match prop, no protocol, not regexp');

assert.strictEqual(parseMatchProps(URL2).url, URL2, 'No url match prop, has protocol, not regexp');
assert.strictEqual(parseMatchProps(`url:${URL2}`).url, URL2, 'url match prop, has protocol, not regexp');

assert.strictEqual(parseMatchProps(URL3).url, URL3, 'No url match prop, has protocol, regexp');
assert.strictEqual(parseMatchProps(`url:${URL3}`).url, URL3, 'url match prop, has protocol, regexp');

assert.strictEqual(parseMatchProps(URL4).url, URL4, 'No url match prop, has protocol, regexp, extra colon in url');
assert.strictEqual(parseMatchProps(`url:${URL4}`).url, URL4, 'url match prop, has protocol, extra colon in url');
});

test('Test parseMatchProps with different url props, mixed input', (assert) => {
const INPUT1 = `${URL1} ${METHOD_PROP}:${GET_METHOD}`;
const expected1 = {
url: URL1,
[METHOD_PROP]: GET_METHOD,
};
assert.deepEqual(parseMatchProps(INPUT1), expected1, 'No url match prop, no protocol, not regexp');

const INPUT1_PREFIXED = `${URL_PROP}:${URL1} ${METHOD_PROP}:${GET_METHOD}`;
const expectedPrefixed1 = {
url: URL1,
[METHOD_PROP]: GET_METHOD,
};
assert.deepEqual(parseMatchProps(INPUT1_PREFIXED), expectedPrefixed1, 'Has url match prop, no protocol, not regexp');

const INPUT2 = `${URL2} ${METHOD_PROP}:${GET_METHOD}`;
const expected2 = {
url: URL2,
[METHOD_PROP]: GET_METHOD,
};
assert.deepEqual(parseMatchProps(INPUT2), expected2, 'No url match prop, has protocol, not regexp');

const INPUT2_PREFIXED = `${URL_PROP}:${URL2} ${METHOD_PROP}:${GET_METHOD}`;
const expectedPrefixed2 = {
url: URL2,
[METHOD_PROP]: GET_METHOD,
};
assert.deepEqual(parseMatchProps(INPUT2_PREFIXED), expectedPrefixed2, 'Has url match prop, has protocol, not regexp');

const INPUT3 = `${URL3} ${METHOD_PROP}:${GET_METHOD}`;
const expected3 = {
url: URL3,
[METHOD_PROP]: GET_METHOD,
};
assert.deepEqual(parseMatchProps(INPUT3), expected3, 'No url match prop, has protocol, regexp');

const INPUT3_PREFIXED = `${URL_PROP}:${URL3} ${METHOD_PROP}:${GET_METHOD}`;
const expectedPrefixed3 = {
url: URL3,
[METHOD_PROP]: GET_METHOD,
};
assert.deepEqual(parseMatchProps(INPUT3_PREFIXED), expectedPrefixed3, 'Has url match prop, has protocol, regexp');

const INPUT4 = `${URL4} ${METHOD_PROP}:${GET_METHOD}`;
const expected4 = {
url: URL4,
[METHOD_PROP]: GET_METHOD,
};
assert.deepEqual(parseMatchProps(INPUT4), expected4, 'No url match prop, has protocol, regexp, extra colon in url');

const INPUT4_PREFIXED = `${URL_PROP}:${URL4} ${METHOD_PROP}:${GET_METHOD}`;
const expectedPrefixed4 = {
url: URL4,
[METHOD_PROP]: GET_METHOD,
};
assert.deepEqual(parseMatchProps(INPUT4_PREFIXED), expectedPrefixed4, 'Has url match prop, has protocol, regexp, extra colon in url');
});
115 changes: 5 additions & 110 deletions tests/helpers/index.test.js
Original file line number Diff line number Diff line change
@@ -1,110 +1,5 @@
import {
toRegExp,
getNumberFromString,
noopPromiseResolve,
matchStackTrace,
} from '../../src/helpers';

const { test, module } = QUnit;
const name = 'scriptlets-redirects helpers';

module(name);

test('Test toRegExp for valid inputs', (assert) => {
const DEFAULT_VALUE = '.?';
const defaultRegexp = new RegExp(DEFAULT_VALUE);
let inputStr;
let expRegex;

inputStr = '/abc/';
expRegex = /abc/;
assert.deepEqual(toRegExp(inputStr), expRegex);

inputStr = '/[a-z]{1,9}/';
expRegex = /[a-z]{1,9}/;
assert.deepEqual(toRegExp(inputStr), expRegex);

inputStr = '';
assert.deepEqual(toRegExp(inputStr), defaultRegexp);
});

test('Test toRegExp for invalid inputs', (assert) => {
let inputStr;

assert.throws(() => {
inputStr = '/\\/';
toRegExp(inputStr);
});

assert.throws(() => {
inputStr = '/[/';
toRegExp(inputStr);
});

assert.throws(() => {
inputStr = '/*/';
toRegExp(inputStr);
});

assert.throws(() => {
inputStr = '/[0-9]++/';
toRegExp(inputStr);
});
});

test('Test getNumberFromString for all data types inputs', (assert) => {
let inputValue;

// Boolean
inputValue = true;
assert.strictEqual(getNumberFromString(inputValue), null);

// null
inputValue = null;
assert.strictEqual(getNumberFromString(inputValue), null);

// undefined
inputValue = undefined;
assert.strictEqual(getNumberFromString(inputValue), null);

// undefined
inputValue = undefined;
assert.strictEqual(getNumberFromString(inputValue), null);

// number
inputValue = 123;
assert.strictEqual(getNumberFromString(inputValue), 123);

// valid string
inputValue = '123parsable';
assert.strictEqual(getNumberFromString(inputValue), 123);

// invalid string
inputValue = 'not parsable 123';
assert.strictEqual(getNumberFromString(inputValue), null);

// object
inputValue = { test: 'test' };
assert.strictEqual(getNumberFromString(inputValue), null);

// array
inputValue = ['test'];
assert.strictEqual(getNumberFromString(inputValue), null);
});

test('Test noopPromiseResolve for valid response.body values', async (assert) => {
const objResponse = await noopPromiseResolve('{}');
const objBody = await objResponse.json();

const arrResponse = await noopPromiseResolve('[]');
const arrBody = await arrResponse.json();

assert.ok(typeof objBody === 'object' && !objBody.length);
assert.ok(Array.isArray(arrBody) && !arrBody.length);
});

test('Test matchStackTrace for working with getNativeRegexpTest helper', async (assert) => {
const match = matchStackTrace('stack', new Error().stack);

assert.ok(!match);
});
import './get-number-from-string.test';
import './match-stack-trace.test';
import './noop-promise-resolve.test';
import './parse-match-props.test';
import './to-regexp.test';
12 changes: 12 additions & 0 deletions tests/helpers/match-stack.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { matchStackTrace } from '../../src/helpers';

const { test, module } = QUnit;
const name = 'scriptlets-redirects helpers';

module(name);

test('Test matchStackTrace for working with getNativeRegexpTest helper', async (assert) => {
const match = matchStackTrace('stack', new Error().stack);

assert.ok(!match);
});
24 changes: 24 additions & 0 deletions tests/helpers/noop.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { noopPromiseResolve } from '../../src/helpers';

const { test, module } = QUnit;
const name = 'scriptlets-redirects helpers';

module(name);

test('Test noopPromiseResolve for valid response props', async (assert) => {
const TEST_URL = 'url';
const TEST_TYPE = 'opaque';
const objResponse = await noopPromiseResolve('{}');
const objBody = await objResponse.json();

const arrResponse = await noopPromiseResolve('[]');
const arrBody = await arrResponse.json();

const responseWithUrl = await noopPromiseResolve('{}', TEST_URL);
const responseWithType = await noopPromiseResolve('{}', '', TEST_TYPE);

assert.ok(responseWithUrl.url === TEST_URL);
assert.ok(typeof objBody === 'object' && !objBody.length);
assert.ok(Array.isArray(arrBody) && !arrBody.length);
assert.strictEqual(responseWithType.type, TEST_TYPE);
});
Loading

0 comments on commit d435aac

Please sign in to comment.