Skip to content

Commit

Permalink
[ENGG-1692] feat: warning for CSP errors in dynamic response rules (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
nafees87n committed May 23, 2024
1 parent fea581b commit 245f97f
Show file tree
Hide file tree
Showing 7 changed files with 152 additions and 53 deletions.
2 changes: 2 additions & 0 deletions browser-extension/common/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export const EXTENSION_MESSAGES = {
INIT_SESSION_RECORDER: "initSessionRecorder",
CLIENT_PAGE_LOADED: "clientPageLoaded",
ON_BEFORE_AJAX_REQUEST: "onBeforeAjaxRequest",
ON_ERROR_OCCURRED: "onErrorOccurred",
SAVE_TEST_RULE_RESULT: "saveTestRuleResult",
NOTIFY_TEST_RULE_REPORT_UPDATED: "notifyTestRuleReportUpdated",
TEST_RULE_ON_URL: "testRuleOnUrl",
Expand All @@ -49,6 +50,7 @@ export const CLIENT_MESSAGES = {
NOTIFY_RECORD_UPDATED_IN_POPUP: "notifyRecordUpdatedInPopup",
NOTIFY_PAGE_LOADED_FROM_CACHE: "notifyPageLoadedFromCache",
ON_BEFORE_AJAX_REQUEST_PROCESSED: "onBeforeAjaxRequest:processed",
ON_ERROR_OCCURRED_PROCESSED: "onErrorOccurred:processed",
START_EXPLICIT_RULE_TESTING: "startExplicitRuleTesting",
START_IMPLICIT_RULE_TESTING: "startImplicitRuleTesting",
SYNC_APPLIED_RULES: "syncAppliedRules",
Expand Down
144 changes: 93 additions & 51 deletions browser-extension/mv3/src/client-scripts/ajaxRequestInterceptor.js
Original file line number Diff line number Diff line change
Expand Up @@ -183,16 +183,34 @@ import { PUBLIC_NAMESPACE } from "common/constants";
return responseModification.type === "static" && responseModification.serveWithoutRequest;
};

const getFunctionFromCode = (code) => {
return new Function("args", `return (${code})(args);`);
let logShown = false;
const getFunctionFromCode = (code, ruleType) => {
try {
return new Function("args", `return (${code})(args);`);
} catch (e) {
notifyOnErrorOccurred({
initiatorDomain: location.origin,
url: location.href,
}).then(() => {
if (!logShown) {
logShown = true;
console.log(
`%cRequestly%c Please reload the page for ${ruleType} rule to take effect`,
"color: #3c89e8; padding: 1px 5px; border-radius: 4px; border: 1px solid #91caff;",
"color: red; font-style: italic"
);
}
});
return () => {};
}
};

const getCustomRequestBody = (requestRuleData, args) => {
let requestBody;
if (requestRuleData.request.type === "static") {
requestBody = requestRuleData.request.value;
} else {
requestBody = getFunctionFromCode(requestRuleData.request.value)(args);
requestBody = getFunctionFromCode(requestRuleData.request.value, "request")(args);
}

if (typeof requestBody !== "object" || isNonJsonObject(requestBody)) {
Expand Down Expand Up @@ -276,35 +294,43 @@ import { PUBLIC_NAMESPACE } from "common/constants";

const isContentTypeJSON = (contentType) => !!contentType?.includes("application/json");

const notifyOnBeforeRequest = async (requestDetails) => {
const postMessageAndWaitForAck = async (message, action) => {
window.postMessage(
{
...message,
action,
source: "requestly:client",
action: "onBeforeAjaxRequest",
requestDetails,
},
window.location.href
);

let onBeforeAjaxRequestAckHandler;
let ackHandler;

const ackAction = `${action}:processed`;

return Promise.race([
new Promise((resolve) => setTimeout(resolve, 2000)),
new Promise((resolve) => {
setTimeout(resolve, 2000);
}),
new Promise((resolve) => {
onBeforeAjaxRequestAckHandler = (event) => {
if (event.data.action === "onBeforeAjaxRequest:processed") {
ackHandler = (event) => {
if (event.data.action === ackAction) {
resolve();
}
};
window.addEventListener("message", onBeforeAjaxRequestAckHandler);
window.addEventListener("message", ackHandler);
}),
]).finally(() => {
window.removeEventListener("message", onBeforeAjaxRequestAckHandler);
window.removeEventListener("message", ackHandler);
});
};

const notifyOnBeforeRequest = async (requestDetails) => {
return postMessageAndWaitForAck({ requestDetails }, "onBeforeAjaxRequest");
};

const notifyOnErrorOccurred = async (requestDetails) => {
return postMessageAndWaitForAck({ requestDetails }, "onErrorOccurred");
};

/**
* ********** Within Context Functions end here *************
*/
Expand Down Expand Up @@ -359,7 +385,10 @@ import { PUBLIC_NAMESPACE } from "common/constants";
if (this.readyState === this.DONE) {
let customResponse =
responseModification.type === "code"
? getFunctionFromCode(responseRuleData.response.value)({
? getFunctionFromCode(
responseRuleData.response.value,
"response"
)({
method: this.method,
url,
requestHeaders: this.requestHeaders,
Expand All @@ -370,6 +399,10 @@ import { PUBLIC_NAMESPACE } from "common/constants";
})
: responseModification.value;

if (typeof customResponse === "undefined") {
return;
}

// Convert customResponse back to rawText
// response.value is String and evaluator method might return string/object
if (isPromise(customResponse)) {
Expand Down Expand Up @@ -485,15 +518,19 @@ import { PUBLIC_NAMESPACE } from "common/constants";
bodyAsJson: jsonifyValidJSONString(data),
});

notifyRequestRuleApplied({
ruleDetails: requestRule,
requestDetails: {
url: this.requestURL,
method: this.method,
type: "xmlhttprequest",
timeStamp: Date.now(),
},
});
if (typeof this.requestData !== "undefined") {
notifyRequestRuleApplied({
ruleDetails: requestRule,
requestDetails: {
url: this.requestURL,
method: this.method,
type: "xmlhttprequest",
timeStamp: Date.now(),
},
});
} else {
this.requestData = data;
}
}

this.responseRule = getMatchedResponseRule(this.requestURL);
Expand Down Expand Up @@ -542,36 +579,37 @@ import { PUBLIC_NAMESPACE } from "common/constants";

if (requestRuleData) {
const originalRequestBody = await request.text();
const requestBody =
getCustomRequestBody(requestRuleData, {
method,
url,
body: originalRequestBody,
bodyAsJson: jsonifyValidJSONString(originalRequestBody),
}) || {};

request = new Request(request.url, {
const requestBody = getCustomRequestBody(requestRuleData, {
method,
body: requestBody,
headers: request.headers,
referrer: request.referrer,
referrerPolicy: request.referrerPolicy,
mode: request.mode,
credentials: request.credentials,
cache: request.cache,
redirect: request.redirect,
integrity: request.integrity,
url,
body: originalRequestBody,
bodyAsJson: jsonifyValidJSONString(originalRequestBody),
});

notifyRequestRuleApplied({
ruleDetails: requestRuleData,
requestDetails: {
url,
if (typeof requestBody !== undefined) {
request = new Request(request.url, {
method,
type: "fetch",
timeStamp: Date.now(),
},
});
body: requestBody ?? {},
headers: request.headers,
referrer: request.referrer,
referrerPolicy: request.referrerPolicy,
mode: request.mode,
credentials: request.credentials,
cache: request.cache,
redirect: request.redirect,
integrity: request.integrity,
});

notifyRequestRuleApplied({
ruleDetails: requestRuleData,
requestDetails: {
url,
method,
type: "fetch",
timeStamp: Date.now(),
},
});
}
}

let requestData;
Expand Down Expand Up @@ -682,7 +720,11 @@ import { PUBLIC_NAMESPACE } from "common/constants";
};
}

customResponse = getFunctionFromCode(responseRuleData.response.value)(evaluatorArgs);
customResponse = getFunctionFromCode(responseRuleData.response.value, "response")(evaluatorArgs);

if (typeof customResponse === "undefined") {
return fetchedResponse;
}

// evaluator might return us Object but response.value is string
// So make the response consistent by converting to JSON String and then create the Response object
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,17 @@ export const initPageScriptMessageListener = () => {
);
});
break;
case EXTENSION_MESSAGES.ON_ERROR_OCCURRED:
chrome.runtime.sendMessage(event.data, () => {
window.postMessage(
{
source: "requestly:client",
action: CLIENT_MESSAGES.ON_ERROR_OCCURRED_PROCESSED,
},
window.location.href
);
});
break;
}
});
};
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,10 @@ export const initMessageHandler = () => {
requestProcessor.onBeforeAJAXRequest(sender.tab.id, message.requestDetails).then(sendResponse);
return true;

case EXTENSION_MESSAGES.ON_ERROR_OCCURRED:
requestProcessor.onErrorOccurred(sender.tab.id, message.requestDetails).then(sendResponse);
return true;

case EXTENSION_MESSAGES.TEST_RULE_ON_URL:
launchUrlAndStartRuleTesting(message, sender.tab.id);
break;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { AJAXRequestDetails, SessionRuleType } from "./types";
import { updateRequestSpecificRules } from "../rulesManager";

export const handleCSPError = async (tabId: number, requestDetails: AJAXRequestDetails): Promise<void> => {
await updateRequestSpecificRules(
tabId,
requestDetails.initiatorDomain,
{
action: {
type: chrome.declarativeNetRequest.RuleActionType.MODIFY_HEADERS,
responseHeaders: [
{
header: "Content-Security-Policy",
operation: chrome.declarativeNetRequest.HeaderOperation.REMOVE,
},
],
},
condition: {
urlFilter: requestDetails.initiatorDomain,
resourceTypes: [
chrome.declarativeNetRequest.ResourceType.SUB_FRAME,
chrome.declarativeNetRequest.ResourceType.MAIN_FRAME,
],
tabIds: [tabId],
excludedInitiatorDomains: ["requestly.io", "requestly.com"],
},
},
SessionRuleType.CSP_ERROR
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { forwardHeadersOnRedirect } from "./handleHeadersOnRedirect";
import { handleInitiatorDomainFunction } from "./handleInitiatorDomainFunction";
import rulesStorageService from "../../../rulesStorageService";
import { RuleType } from "common/types";
import { handleCSPError } from "./handleCSPError";

class RequestProcessor {
constructor() {}
Expand All @@ -17,13 +18,21 @@ class RequestProcessor {
const redirectReplaceRules = enabledRules.filter(
(rule) => rule.ruleType === RuleType.REDIRECT || rule.ruleType === RuleType.REPLACE
);

const headerRules = enabledRules.filter((rule) => rule.ruleType === RuleType.HEADERS);

await forwardHeadersOnRedirect(tabId, requestDetails, redirectReplaceRules);

await handleInitiatorDomainFunction(tabId, requestDetails, headerRules);
};

onErrorOccurred = async (tabId: number, requestDetails: AJAXRequestDetails): Promise<void> => {
const enabledRules = await rulesStorageService.getEnabledRules();

if (enabledRules.length === 0) {
return;
}

await handleCSPError(tabId, requestDetails);
};
}

export const requestProcessor = new RequestProcessor();
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ export interface AJAXRequestDetails {
export enum SessionRuleType {
FORWARD_IGNORED_HEADERS = "forwardIgnoredHeaders",
INITIATOR_DOMAIN = "initiatorDomain",
CSP_ERROR = "cspError",
}

0 comments on commit 245f97f

Please sign in to comment.