From 15f743dabb117b263d77b361227c4a7b6a384710 Mon Sep 17 00:00:00 2001 From: Stanislav A Date: Thu, 20 Oct 2022 18:53:10 +0300 Subject: [PATCH] redo replacement logic, move matching to matchRequestProps helper --- src/helpers/index.js | 3 +- src/helpers/match-request-props.js | 35 +++++ .../{fetch-utils.js => request-utils.js} | 19 +++ .../trusted-replace-xhr-response.js | 134 +++++++++++------- 4 files changed, 142 insertions(+), 49 deletions(-) create mode 100644 src/helpers/match-request-props.js rename src/helpers/{fetch-utils.js => request-utils.js} (88%) diff --git a/src/helpers/index.js b/src/helpers/index.js index 110df45bf..56700c1cc 100644 --- a/src/helpers/index.js +++ b/src/helpers/index.js @@ -17,7 +17,7 @@ export * from './array-utils'; export * from './cookie-utils'; export * from './number-utils'; export * from './adjust-set-utils'; -export * from './fetch-utils'; +export * from './request-utils'; export * from './object-utils'; export * from './prevent-window-open-utils'; export * from './add-event-listener-utils'; @@ -26,3 +26,4 @@ export * from './regexp-utils'; export * from './random-response'; export * from './get-descriptor-addon'; export * from './parse-flags'; +export * from './match-request-props'; diff --git a/src/helpers/match-request-props.js b/src/helpers/match-request-props.js new file mode 100644 index 000000000..d8de05df0 --- /dev/null +++ b/src/helpers/match-request-props.js @@ -0,0 +1,35 @@ +import { + getMatchPropsData, + validateParsedData, + parseMatchProps, +} from './request-utils'; + +/** + * Checks if given propsToMatch string matches with given request data + * This is used by prevent-xhr, prevent-fetch, trusted-replace-xhr-response + * and trusted-replace-fetch-response scriptlets + * @param {string} propsToMatch + * @param {Object} requestData object with standard properties of fetch/xhr like url, method etc + * @returns {boolean} + */ +export const matchRequestProps = (propsToMatch, requestData) => { + let isMatched; + + const parsedData = parseMatchProps(propsToMatch); + if (!validateParsedData(parsedData)) { + // eslint-disable-next-line no-console + console.log(`Invalid parameter: ${propsToMatch}`); + isMatched = false; + } else { + const matchData = getMatchPropsData(parsedData); + // prevent only if all props match + isMatched = Object.keys(matchData) + .every((matchKey) => { + const matchValue = matchData[matchKey]; + return Object.prototype.hasOwnProperty.call(requestData, matchKey) + && matchValue.test(requestData[matchKey]); + }); + } + + return isMatched; +}; diff --git a/src/helpers/fetch-utils.js b/src/helpers/request-utils.js similarity index 88% rename from src/helpers/fetch-utils.js rename to src/helpers/request-utils.js index 0fd3dcbb7..b80370e3a 100644 --- a/src/helpers/fetch-utils.js +++ b/src/helpers/request-utils.js @@ -58,6 +58,25 @@ export const getFetchData = (args) => { return fetchPropsObj; }; +/** + * Collect xhr.open arguments to object + * @param {string} method + * @param {string} url + * @param {string} async + * @param {string} user + * @param {string} password + * @returns {Object} + */ +export const getXhrData = (method, url, async, user, password) => { + return { + method, + url, + async, + user, + password, + }; +}; + /** * Parse propsToMatch input string into object; * used for prevent-fetch and prevent-xhr diff --git a/src/scriptlets/trusted-replace-xhr-response.js b/src/scriptlets/trusted-replace-xhr-response.js index 7c08fbd5d..f3af61a41 100644 --- a/src/scriptlets/trusted-replace-xhr-response.js +++ b/src/scriptlets/trusted-replace-xhr-response.js @@ -5,6 +5,8 @@ import { parseMatchProps, validateParsedData, getMatchPropsData, + matchRequestProps, + getXhrData, // following helpers should be imported and injected // because they are used by helpers above toRegExp, @@ -69,7 +71,7 @@ import { * ``` */ /* eslint-enable max-len */ -export function trustedReplaceXhrResponse(source, pattern, replacement, propsToMatch) { +export function trustedReplaceXhrResponse(source, pattern = '', replacement = '', propsToMatch) { // do nothing if browser does not support Proxy (e.g. Internet Explorer) // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy if (typeof Proxy === 'undefined') { @@ -80,37 +82,38 @@ export function trustedReplaceXhrResponse(source, pattern, replacement, propsToM return; } + const origOpen = window.XMLHttpRequest.prototype.open; + const origSend = window.XMLHttpRequest.prototype.send; + const MATCH_ALL_CHARACTERS_REGEX = /[\s\S]/; let shouldReplace = false; - let responseUrl; + let xhrData; + const requestHeaders = []; + const openWrapper = (target, thisArg, args) => { - // Get method and url from .open() - const xhrData = { - method: args[0], - url: args[1], - }; - responseUrl = xhrData.url; + xhrData = getXhrData(...args); + if (pattern === '' && replacement === '') { // Log if no propsToMatch given const logMessage = `log: xhr( ${objectToString(xhrData)} )`; hit(source, logMessage); } else { - const parsedData = parseMatchProps(propsToMatch); - if (!validateParsedData(parsedData)) { - // eslint-disable-next-line no-console - console.log(`Invalid parameter: ${propsToMatch}`); - shouldReplace = false; - } else { - const matchData = getMatchPropsData(parsedData); - // prevent only if all props match - shouldReplace = Object.keys(matchData) - .every((matchKey) => { - const matchValue = matchData[matchKey]; - return Object.prototype.hasOwnProperty.call(xhrData, matchKey) - && matchValue.test(xhrData[matchKey]); - }); - } + shouldReplace = matchRequestProps(propsToMatch, xhrData); + } + + // Trap setRequestHeader of target xhr object to mimic request headers later + if (shouldReplace) { + const setRequestHeaderWrapper = (target, thisArg, args) => { + requestHeaders.push(args); + return Reflect.apply(target, thisArg, args); + }; + + const setRequestHeaderHandler = { + apply: setRequestHeaderWrapper, + }; + + thisArg.setRequestHeader = new Proxy(thisArg.setRequestHeader, setRequestHeaderHandler); } return Reflect.apply(target, thisArg, args); @@ -121,35 +124,68 @@ export function trustedReplaceXhrResponse(source, pattern, replacement, propsToM return Reflect.apply(target, thisArg, args); } - const parsedPattern = pattern === getWildcardSymbol() - ? MATCH_ALL_CHARACTERS_REGEX - : pattern; - - const modifiedContent = thisArg.responseText.replace(parsedPattern, replacement); - - // Mock response object - Object.defineProperties(thisArg, { - readyState: { value: 4, writable: false }, - response: { value: modifiedContent, writable: false }, - responseText: { value: modifiedContent, writable: false }, - responseURL: { value: responseUrl, writable: false }, - responseXML: { value: '', writable: false }, - status: { value: 200, writable: false }, - statusText: { value: 'OK', writable: false }, + const secretXhr = new XMLHttpRequest(); + secretXhr.addEventListener('readystatechange', () => { + if (secretXhr.readyState !== 4) { + return; + } + + const { + readyState, + response, + responseText, + responseURL, + responseXML, + status, + statusText, + } = secretXhr; + + const parsedPattern = pattern === getWildcardSymbol() + ? MATCH_ALL_CHARACTERS_REGEX + : pattern; + const content = response || responseText; + + const modifiedContent = content.replace(parsedPattern, replacement); + + // Manually put required values into target XHR object + // as thisArg can't be redefined and XHR objects can't be (re)assigned or copied + Object.defineProperties(thisArg, { + readyState: { value: readyState }, + response: { value: modifiedContent }, + responseText: { value: modifiedContent }, + responseURL: { value: responseURL }, + responseXML: { value: responseXML }, + status: { value: status }, + statusText: { value: statusText }, + }); + + // Mock events + setTimeout(() => { + const stateEvent = new Event('readystatechange'); + thisArg.dispatchEvent(stateEvent); + + const loadEvent = new Event('load'); + thisArg.dispatchEvent(loadEvent); + + const loadEndEvent = new Event('loadend'); + thisArg.dispatchEvent(loadEndEvent); + }, 1); + + hit(source); }); - // Mock events - setTimeout(() => { - const stateEvent = new Event('readystatechange'); - thisArg.dispatchEvent(stateEvent); - const loadEvent = new Event('load'); - thisArg.dispatchEvent(loadEvent); + origOpen.apply(secretXhr, [xhrData.method, xhrData.url]); + + // Mimic request headers before sending + // setRequestHeader can only be called on open xhrs + requestHeaders.forEach((header) => { + const name = header[0]; + const value = header[1]; - const loadEndEvent = new Event('loadend'); - thisArg.dispatchEvent(loadEndEvent); - }, 1); + secretXhr.setRequestHeader(name, value); + }); - hit(source); + origSend.call(secretXhr, args); return undefined; }; @@ -175,6 +211,8 @@ trustedReplaceXhrResponse.injections = [ objectToString, getWildcardSymbol, parseMatchProps, + matchRequestProps, + getXhrData, validateParsedData, getMatchPropsData, toRegExp,