Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 35 additions & 1 deletion browser-extension/mv3/src/common/ruleMatcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,13 +127,47 @@ const matchRequestWithRuleSourceFilters = function (
case SourceFilterTypes.RESOURCE_TYPE:
return values.includes(requestDetails.type);
case SourceFilterTypes.REQUEST_PAYLOAD:
return matchRequestPayload(values, requestDetails.requestData);
return matchRequestPayload(values, getRequestPayloadForMatching(requestDetails));
default:
return true;
}
});
};

// Picks the object that request payload filters (e.g. GraphQL operationName) should match against.
// POST style requests keep their JSON body. GET/HEAD requests carry no body, so their payload
// (like the GraphQL operationName) lives in the URL query params instead. This mirrors how
// getGraphQLDetails in harLogs/utils.ts reads GQL details from the query string on GET requests.
const getRequestPayloadForMatching = (requestDetails: AJAXRequestDetails) => {
const { requestData, url, method } = requestDetails;

if (requestData && typeof requestData === "object" && Object.keys(requestData).length > 0) {
return requestData;
}

const normalizedMethod = (method || "").toUpperCase();
if (normalizedMethod === "GET" || normalizedMethod === "HEAD") {
return parseQueryParams(url);
}

return requestData;
};

const parseQueryParams = (url: string): Record<string, string> => {
if (!url) return {};

try {
const searchParams = new URL(url).searchParams;
const queryParams: Record<string, string> = {};
searchParams.forEach((value, key) => {
queryParams[key] = value;
});
return queryParams;
} catch (e) {
return {};
}
};

const matchRequestPayload = (requestPayloadFilter: RequestPayloadFilter, requestData: any) => {
if (!requestPayloadFilter) return true;
if (typeof requestPayloadFilter === "object" && Object.keys(requestPayloadFilter).length === 0) return true;
Expand Down
46 changes: 41 additions & 5 deletions common/rule-processor/src/RuleHelper.js
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ class RuleMatcher {
let destination = typeof pair.destination !== "undefined" ? pair.destination : null;
let newResultingUrl = null;

if (RuleMatcher.matchRequestWithRuleSourceFilters(pair.source.filters, requestDetails)) {
if (RuleMatcher.matchRequestWithRuleSourceFilters(pair.source.filters, requestDetails, url)) {
newResultingUrl = RuleMatcher.matchUrlWithRuleSource(pair.source, url, destination);
}

Expand Down Expand Up @@ -185,7 +185,7 @@ class RuleMatcher {
return resultingUrl !== url ? resultingUrl : null;
}

static matchRequestWithRuleSourceFilters(sourceFilters, requestDetails) {
static matchRequestWithRuleSourceFilters(sourceFilters, requestDetails, url) {
if (!sourceFilters || !requestDetails) {
return true;
}
Expand Down Expand Up @@ -225,12 +225,16 @@ class RuleMatcher {
break;

case CONSTANTS.RULE_SOURCE_FILTER_TYPES.REQUEST_DATA:
// eslint-disable-next-line no-case-declarations
const requestPayload = RuleMatcher.getRequestPayloadForMatching(
requestDetails.requestData,
url,
requestDetails.method
);
if (
// although currently only accepts one entry from UI
// but this is to be compatible with changes made to accept array of filter values
!filterValues.some((filterValue) =>
this.isRequestPayloadFilterApplicable(requestDetails.requestData, filterValue)
)
!filterValues.some((filterValue) => this.isRequestPayloadFilterApplicable(requestPayload, filterValue))
) {
return false;
}
Expand Down Expand Up @@ -391,6 +395,38 @@ class RuleMatcher {

return false;
}

// Picks the object that request payload filters (e.g. GraphQL operationName) should match against.
// POST style requests keep their JSON body. GET/HEAD requests carry no body, so their payload
// (like the GraphQL operationName) lives in the URL query params instead. This mirrors how
// getGraphQLDetails in harLogs/utils.ts reads GQL details from the query string on GET requests.
static getRequestPayloadForMatching(requestData, url, method) {
if (requestData && typeof requestData === "object" && Object.keys(requestData).length > 0) {
return requestData;
}

const normalizedMethod = (method || "").toUpperCase();
if (normalizedMethod === "GET" || normalizedMethod === "HEAD") {
return RuleMatcher.parseQueryParams(url);
}

return requestData;
}

static parseQueryParams(url) {
if (!url) return {};

try {
const searchParams = new URL(url).searchParams;
const queryParams = {};
searchParams.forEach((value, key) => {
queryParams[key] = value;
});
return queryParams;
} catch (e) {
return {};
}
}
}

export default RuleMatcher;
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ class DelayRequestRuleProcessor {

// If Source does not match, proceed with next pair
if (
!RuleHelper.matchRequestWithRuleSourceFilters(pair.source.filters, details) ||
!RuleHelper.matchRequestWithRuleSourceFilters(pair.source.filters, details, requestURL) ||
RuleHelper.matchUrlWithRuleSource(pair.source, requestURL) === null
) {
continue;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ class ReplaceRuleProcessor {
pair = pairs[i];
pair.from = pair.from || "";

if (pair.source && !RuleHelper.matchRequestWithRuleSourceFilters(pair.source.filters, details)) {
if (pair.source && !RuleHelper.matchRequestWithRuleSourceFilters(pair.source.filters, details, requestURL)) {
continue;
}

Expand Down
44 changes: 44 additions & 0 deletions common/rule-processor/tests/RuleHelper.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,50 @@ describe("RuleHelper: ", function () {
expect(RuleHelper.matchRequestWithRuleSourceFilters(sourceFilters, requestDetails)).toBeFalsy();
});
});

describe("#matchRequestWithRuleSourceFilters (request payload from query params)", function () {
// GraphQL operationName targeting is a request payload filter with key "operationName".
const gqlSourceFilters = {
[CONSTANTS.RULE_SOURCE_FILTER_TYPES.REQUEST_DATA]: {
key: "operationName",
value: "GetUser",
},
};

it("should match a GET GraphQL request whose operationName is in the query params", function () {
const requestDetails = {
method: "GET",
type: "xmlhttprequest",
pageUrl: URL_SOURCES.EXAMPLE,
};
const url = "https://example.com/graphql?operationName=GetUser&variables=%7B%7D";

expect(RuleHelper.matchRequestWithRuleSourceFilters(gqlSourceFilters, requestDetails, url)).toBeTruthy();
});

it("should not match when the query param operationName is different", function () {
const requestDetails = {
method: "GET",
type: "xmlhttprequest",
pageUrl: URL_SOURCES.EXAMPLE,
};
const url = "https://example.com/graphql?operationName=GetTeams";

expect(RuleHelper.matchRequestWithRuleSourceFilters(gqlSourceFilters, requestDetails, url)).toBeFalsy();
});

it("should still match a POST GraphQL request using the request body", function () {
const requestDetails = {
method: "POST",
type: "xmlhttprequest",
pageUrl: URL_SOURCES.EXAMPLE,
requestData: { operationName: "GetUser", variables: {} },
};
const url = "https://example.com/graphql";

expect(RuleHelper.matchRequestWithRuleSourceFilters(gqlSourceFilters, requestDetails, url)).toBeTruthy();
});
});
});

describe("#headerModificationTypes", function () {
Expand Down